@jsenv/navi 0.29.100 → 0.29.102
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 +649 -46
- package/dist/jsenv_navi.js.map +21 -18
- package/docs/control_group.md +14 -9
- package/docs/interactions.md +14 -0
- package/docs/navigation.md +127 -0
- package/docs/z_index.md +8 -8
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -49,6 +49,16 @@ const css$15 = /* css */`
|
|
|
49
49
|
--navi-z-index-control-hovered: 1;
|
|
50
50
|
--navi-z-index-control-focused: 2;
|
|
51
51
|
|
|
52
|
+
/* A control the user is not on and yet still has to paint in front: one
|
|
53
|
+
holding something open (a picker showing its list, an expandable
|
|
54
|
+
header). Its border keeps the color the open state gives it, while
|
|
55
|
+
hover and focus have both moved on — into the popup, or onto whatever
|
|
56
|
+
the pointer travelled to next — so neither of the two values above is
|
|
57
|
+
there to raise it, and the neighbour painted after it cuts the very
|
|
58
|
+
border that says what is open. Above both: the member holding the
|
|
59
|
+
popup open outranks a member merely hovered or focused. */
|
|
60
|
+
--navi-z-index-control-expanded: 3;
|
|
61
|
+
|
|
52
62
|
/* Kept stuck while something scrolls under it: a list header, the head
|
|
53
63
|
and foot of a side panel, a table's sticky cells, the header and
|
|
54
64
|
footer of any scrolling Box. Above raised controls — a control
|
|
@@ -2819,7 +2829,10 @@ const readSignalForUrlBuild = (connection) => {
|
|
|
2819
2829
|
/**
|
|
2820
2830
|
* Creates a custom route pattern matcher
|
|
2821
2831
|
*/
|
|
2822
|
-
const createRoutePattern = (
|
|
2832
|
+
const createRoutePattern = (
|
|
2833
|
+
pattern,
|
|
2834
|
+
{ searchParams = {}, params: paramConstraints = {} } = {},
|
|
2835
|
+
) => {
|
|
2823
2836
|
// Detect and process path signals in the pattern
|
|
2824
2837
|
const [cleanPattern, pathConnections] = detectSignals(pattern);
|
|
2825
2838
|
|
|
@@ -2844,9 +2857,11 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2844
2857
|
// All connections (path + query) for ancestor/descendant signal resolution
|
|
2845
2858
|
const connections = [...pathConnections, ...queryConnectionMap.values()];
|
|
2846
2859
|
|
|
2860
|
+
const paramConstraintMap = createParamConstraintMap(paramConstraints);
|
|
2847
2861
|
const parsedPattern = parsePattern(cleanPattern, {
|
|
2848
2862
|
pathConnectionMap,
|
|
2849
2863
|
queryConnectionMap,
|
|
2864
|
+
paramConstraintMap,
|
|
2850
2865
|
});
|
|
2851
2866
|
|
|
2852
2867
|
debug$3(`[CustomPattern] Created pattern:`, parsedPattern);
|
|
@@ -2854,11 +2869,21 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2854
2869
|
debug$3(`[CustomPattern] Path connections:`, pathConnectionMap.size);
|
|
2855
2870
|
debug$3(`[CustomPattern] Query connections:`, queryConnectionMap.size);
|
|
2856
2871
|
|
|
2857
|
-
|
|
2872
|
+
/**
|
|
2873
|
+
* @param {string} url
|
|
2874
|
+
* @param {object} [options]
|
|
2875
|
+
* @param {boolean} [options.exact] - Only match the url this pattern is the
|
|
2876
|
+
* address of: a trailing slash or a wildcard stops catching what lies below
|
|
2877
|
+
* it. What "/" means for a container ("everything under me") and what it
|
|
2878
|
+
* means for a redirection ("that very address") are not the same question.
|
|
2879
|
+
*/
|
|
2880
|
+
const applyOn = (url, { exact = false } = {}) => {
|
|
2858
2881
|
const result = matchUrl(parsedPattern, url, {
|
|
2859
2882
|
baseUrl,
|
|
2860
2883
|
baseFileUrl,
|
|
2861
2884
|
queryConnectionMap,
|
|
2885
|
+
paramConstraintMap,
|
|
2886
|
+
exact,
|
|
2862
2887
|
patternObj: patternObject,
|
|
2863
2888
|
});
|
|
2864
2889
|
|
|
@@ -3524,7 +3549,9 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3524
3549
|
const entryIsMeaningful = (conn) => {
|
|
3525
3550
|
const entry = intended.get(conn.paramName);
|
|
3526
3551
|
return (
|
|
3527
|
-
Boolean(entry) &&
|
|
3552
|
+
Boolean(entry) &&
|
|
3553
|
+
entry.value !== undefined &&
|
|
3554
|
+
conn.isCustomValue(entry.value)
|
|
3528
3555
|
);
|
|
3529
3556
|
};
|
|
3530
3557
|
if (ancestorPattern !== patternObject.parent) {
|
|
@@ -3714,6 +3741,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3714
3741
|
connections,
|
|
3715
3742
|
pathConnectionMap, // Separate map for path parameters
|
|
3716
3743
|
queryConnectionMap, // Separate map for query parameters
|
|
3744
|
+
paramConstraintMap, // Which values each param accepts (Map<paramName, test>)
|
|
3717
3745
|
parsedPattern,
|
|
3718
3746
|
children: [],
|
|
3719
3747
|
parent: null,
|
|
@@ -3821,10 +3849,60 @@ const detectSignals = (routePattern) => {
|
|
|
3821
3849
|
return [updatedPattern, signalConnections];
|
|
3822
3850
|
};
|
|
3823
3851
|
|
|
3852
|
+
const EMPTY_PARAM_CONSTRAINT_MAP = new Map();
|
|
3853
|
+
|
|
3854
|
+
/**
|
|
3855
|
+
* Turns `{ gameId: /^W-[A-Z0-9]{8}$/i }` into `Map<paramName, (value) => boolean>`.
|
|
3856
|
+
* A constraint is a regexp, a list of accepted values, or a predicate.
|
|
3857
|
+
* A constraint decides which url segments the param accepts; a segment it
|
|
3858
|
+
* declines makes the whole route a non-match, so only path params can carry
|
|
3859
|
+
* one — a search param is extracted from a url the path already matched.
|
|
3860
|
+
*/
|
|
3861
|
+
const createParamConstraintMap = (paramConstraints) => {
|
|
3862
|
+
const entries = Object.entries(paramConstraints);
|
|
3863
|
+
if (entries.length === 0) {
|
|
3864
|
+
return EMPTY_PARAM_CONSTRAINT_MAP;
|
|
3865
|
+
}
|
|
3866
|
+
const paramConstraintMap = new Map();
|
|
3867
|
+
for (const [paramName, constraint] of entries) {
|
|
3868
|
+
if (constraint instanceof RegExp) {
|
|
3869
|
+
paramConstraintMap.set(paramName, (value) => {
|
|
3870
|
+
constraint.lastIndex = 0; // a /g or /y regexp would otherwise resume where the previous test stopped
|
|
3871
|
+
return constraint.test(value);
|
|
3872
|
+
});
|
|
3873
|
+
continue;
|
|
3874
|
+
}
|
|
3875
|
+
if (Array.isArray(constraint)) {
|
|
3876
|
+
// a url segment is a string, so the list is compared as strings:
|
|
3877
|
+
// `oneOf: [1, 2, 3]` accepts "/2"
|
|
3878
|
+
const acceptedValueSet = new Set(
|
|
3879
|
+
constraint.map((value) => String(value)),
|
|
3880
|
+
);
|
|
3881
|
+
paramConstraintMap.set(paramName, (value) => acceptedValueSet.has(value));
|
|
3882
|
+
continue;
|
|
3883
|
+
}
|
|
3884
|
+
if (typeof constraint === "function") {
|
|
3885
|
+
paramConstraintMap.set(paramName, (value) => Boolean(constraint(value)));
|
|
3886
|
+
continue;
|
|
3887
|
+
}
|
|
3888
|
+
throw new TypeError(
|
|
3889
|
+
`params.${paramName} must be a regexp, an array of values or a function, got ${constraint}`,
|
|
3890
|
+
);
|
|
3891
|
+
}
|
|
3892
|
+
return paramConstraintMap;
|
|
3893
|
+
};
|
|
3894
|
+
|
|
3824
3895
|
/**
|
|
3825
3896
|
* Parse a route pattern string into structured segments
|
|
3826
3897
|
*/
|
|
3827
|
-
const parsePattern = (
|
|
3898
|
+
const parsePattern = (
|
|
3899
|
+
pattern,
|
|
3900
|
+
{
|
|
3901
|
+
pathConnectionMap,
|
|
3902
|
+
queryConnectionMap,
|
|
3903
|
+
paramConstraintMap = EMPTY_PARAM_CONSTRAINT_MAP,
|
|
3904
|
+
},
|
|
3905
|
+
) => {
|
|
3828
3906
|
// Build queryParams from queryConnectionMap
|
|
3829
3907
|
const queryParams = [];
|
|
3830
3908
|
for (const [paramName, connection] of queryConnectionMap) {
|
|
@@ -3878,13 +3956,14 @@ const parsePattern = (pattern, { pathConnectionMap, queryConnectionMap }) => {
|
|
|
3878
3956
|
// 1. Explicitly marked with ?
|
|
3879
3957
|
// 2. Has a default value
|
|
3880
3958
|
// 3. Connected signal has undefined value and no explicit default (allows /map to match /map/:panel)
|
|
3959
|
+
// — unless the param is constrained: "no segment" is not one of the values it accepts
|
|
3881
3960
|
const connection =
|
|
3882
3961
|
pathConnectionMap.get(paramName) || queryConnectionMap.get(paramName);
|
|
3883
3962
|
const hasDefault =
|
|
3884
3963
|
connection && connection.getDefaultValue() !== undefined;
|
|
3885
3964
|
let isOptional = seg.endsWith("?") || hasDefault;
|
|
3886
3965
|
|
|
3887
|
-
if (!isOptional) {
|
|
3966
|
+
if (!isOptional && !paramConstraintMap.has(paramName)) {
|
|
3888
3967
|
// Check if connected signal has undefined value (making parameter optional for index routes)
|
|
3889
3968
|
if (
|
|
3890
3969
|
connection &&
|
|
@@ -3995,7 +4074,14 @@ const tryExtractChildParameters = (
|
|
|
3995
4074
|
// We need to verify that this parameter isn't already captured by parent
|
|
3996
4075
|
if (!(segment.name in existingParams)) {
|
|
3997
4076
|
const urlSegment = remainingSegments[remainingIndex];
|
|
3998
|
-
|
|
4077
|
+
const paramValue = decodeURIComponent(urlSegment);
|
|
4078
|
+
const paramConstraint = childPattern.paramConstraintMap.get(
|
|
4079
|
+
segment.name,
|
|
4080
|
+
);
|
|
4081
|
+
if (paramConstraint && !paramConstraint(paramValue)) {
|
|
4082
|
+
return null; // the child declines this value, it cannot explain these segments
|
|
4083
|
+
}
|
|
4084
|
+
childParams[segment.name] = paramValue;
|
|
3999
4085
|
remainingIndex++;
|
|
4000
4086
|
}
|
|
4001
4087
|
} else if (
|
|
@@ -4022,7 +4108,14 @@ const tryExtractChildParameters = (
|
|
|
4022
4108
|
const matchUrl = (
|
|
4023
4109
|
parsedPattern,
|
|
4024
4110
|
url,
|
|
4025
|
-
{
|
|
4111
|
+
{
|
|
4112
|
+
baseUrl,
|
|
4113
|
+
baseFileUrl,
|
|
4114
|
+
queryConnectionMap,
|
|
4115
|
+
paramConstraintMap = EMPTY_PARAM_CONSTRAINT_MAP,
|
|
4116
|
+
exact = false,
|
|
4117
|
+
patternObj = null,
|
|
4118
|
+
},
|
|
4026
4119
|
) => {
|
|
4027
4120
|
// Parse the URL
|
|
4028
4121
|
const urlObj = new URL(url, baseUrl);
|
|
@@ -4064,7 +4157,7 @@ const matchUrl = (
|
|
|
4064
4157
|
}
|
|
4065
4158
|
|
|
4066
4159
|
// Root route with trailing slash matches all sub-paths (prefix matching, like other trailing-slash routes)
|
|
4067
|
-
if (parsedPattern.trailingSlash) {
|
|
4160
|
+
if (parsedPattern.trailingSlash && !exact) {
|
|
4068
4161
|
return extractSearchParams(urlObj, queryConnectionMap);
|
|
4069
4162
|
}
|
|
4070
4163
|
|
|
@@ -4133,7 +4226,12 @@ const matchUrl = (
|
|
|
4133
4226
|
|
|
4134
4227
|
// Capture URL segment as parameter value
|
|
4135
4228
|
const urlSeg = urlSegments[urlSegmentIndex];
|
|
4136
|
-
|
|
4229
|
+
const paramValue = decodeURIComponent(urlSeg);
|
|
4230
|
+
const paramConstraint = paramConstraintMap.get(patternSeg.name);
|
|
4231
|
+
if (paramConstraint && !paramConstraint(paramValue)) {
|
|
4232
|
+
return null; // the param declines this value: the route does not match
|
|
4233
|
+
}
|
|
4234
|
+
params[patternSeg.name] = paramValue;
|
|
4137
4235
|
urlSegmentIndex++;
|
|
4138
4236
|
}
|
|
4139
4237
|
}
|
|
@@ -4142,8 +4240,7 @@ const matchUrl = (
|
|
|
4142
4240
|
// Patterns with trailing slashes can match additional URL segments (like wildcards)
|
|
4143
4241
|
// Patterns without trailing slashes should match exactly (unless they're wildcards)
|
|
4144
4242
|
if (
|
|
4145
|
-
!parsedPattern.wildcard &&
|
|
4146
|
-
!parsedPattern.trailingSlash &&
|
|
4243
|
+
(exact || (!parsedPattern.wildcard && !parsedPattern.trailingSlash)) &&
|
|
4147
4244
|
urlSegmentIndex < urlSegments.length
|
|
4148
4245
|
) {
|
|
4149
4246
|
return null; // Pattern without trailing slash/wildcard should not match extra segments
|
|
@@ -4862,8 +4959,61 @@ const getRoutePrivateProperties = (route) => {
|
|
|
4862
4959
|
const ROUTE_NOT_MATCHING_PARAMS = {};
|
|
4863
4960
|
// Flag to prevent signal-to-URL synchronization during URL-to-signal synchronization
|
|
4864
4961
|
let isUpdatingRoutesFromUrl = false;
|
|
4865
|
-
|
|
4866
|
-
|
|
4962
|
+
/**
|
|
4963
|
+
* Declares a route from an url pattern.
|
|
4964
|
+
*
|
|
4965
|
+
* @param {string} pattern - The url pattern, where `:name` is a path param and
|
|
4966
|
+
* `:name=${signal}` binds that param to a signal.
|
|
4967
|
+
* @param {object} [options]
|
|
4968
|
+
* @param {Object<string, import("@preact/signals").Signal>} [options.searchParams]
|
|
4969
|
+
* Search params this route two-way syncs with, by name.
|
|
4970
|
+
* @param {Object<string, RegExp | Array | ((value: string) => boolean)>} [options.params]
|
|
4971
|
+
* Which values a path param accepts, by param name: a regexp tested against the
|
|
4972
|
+
* decoded segment, the list of accepted values (compared as strings, so it can
|
|
4973
|
+
* be the `oneOf` of the signal bound to that param), or a predicate. A route
|
|
4974
|
+
* whose param declines the segment does not match at all: no signal is
|
|
4975
|
+
* written, `<Route fallback>` is reachable, and `/:gameId` can sit at the root
|
|
4976
|
+
* without swallowing `/cgu`. A constrained param is also required — no segment
|
|
4977
|
+
* is not one of the values it accepts — so `/:gameId` does not match `/`.
|
|
4978
|
+
*
|
|
4979
|
+
* Constrain the shape, not the existence: whether that game exists is for the
|
|
4980
|
+
* route action and the page to answer, on a route that did match.
|
|
4981
|
+
*
|
|
4982
|
+
* ```js
|
|
4983
|
+
* route(`/:gameId=${gamePageIdSignal}`, { params: { gameId: /^W-[A-Z0-9]{8}$/i } });
|
|
4984
|
+
* route(`/games/:section=${sectionSignal}`, { params: { section: SECTIONS } });
|
|
4985
|
+
* ```
|
|
4986
|
+
* @param {object} [options.redirectRoute]
|
|
4987
|
+
* This address only sends elsewhere: the route it resolves to. The
|
|
4988
|
+
* redirection happens at the door of the navigation, so this address is never
|
|
4989
|
+
* displayed, never enters the history, runs no route action and writes no
|
|
4990
|
+
* signal — going back lands on the page before it, not on the redirection
|
|
4991
|
+
* again. It fires on this route's own address only: a trailing slash still
|
|
4992
|
+
* catches what lies below it for rendering, never for redirecting.
|
|
4993
|
+
*
|
|
4994
|
+
* The params found in the url carry over to the ones the target route
|
|
4995
|
+
* declares under the same name; what it cannot place is left behind.
|
|
4996
|
+
*
|
|
4997
|
+
* ```js
|
|
4998
|
+
* route("/", { redirectRoute: MY_GAMES_PAGE });
|
|
4999
|
+
* // gameId carries over on its own, shareState is not a param of GAME_PAGE
|
|
5000
|
+
* route("/:gameId/:shareState", { redirectRoute: GAME_PAGE });
|
|
5001
|
+
* ```
|
|
5002
|
+
* @param {object|Function|null} [options.redirectRouteParams]
|
|
5003
|
+
* What to change about the params carried over: an object, or a function of
|
|
5004
|
+
* the params found in the url returning one. Written params win over inherited
|
|
5005
|
+
* ones, `undefined` drops one, and `null` carries nothing over at all.
|
|
5006
|
+
*
|
|
5007
|
+
* ```js
|
|
5008
|
+
* route("/legacy/:id", { redirectRoute: GAME_PAGE, redirectRouteParams: ({ id }) => ({ gameId: id }) });
|
|
5009
|
+
* route("/:gameId/invite", { redirectRoute: HOME_PAGE, redirectRouteParams: null });
|
|
5010
|
+
* ```
|
|
5011
|
+
*/
|
|
5012
|
+
const route = (
|
|
5013
|
+
pattern,
|
|
5014
|
+
{ searchParams, params, redirectRoute, redirectRouteParams } = {},
|
|
5015
|
+
) => {
|
|
5016
|
+
const routePattern = createRoutePattern(pattern, { searchParams, params });
|
|
4867
5017
|
const { cleanPattern } = routePattern;
|
|
4868
5018
|
const [publishStatus, subscribeStatus] = createPubSub();
|
|
4869
5019
|
|
|
@@ -4906,6 +5056,8 @@ const route = (pattern, { searchParams } = {}) => {
|
|
|
4906
5056
|
{
|
|
4907
5057
|
const routePrivateProperties = {
|
|
4908
5058
|
routePattern,
|
|
5059
|
+
redirectRoute,
|
|
5060
|
+
redirectRouteParams,
|
|
4909
5061
|
setup: null,
|
|
4910
5062
|
updateStatus: null,
|
|
4911
5063
|
cleanup: null,
|
|
@@ -5229,6 +5381,113 @@ const route = (pattern, { searchParams } = {}) => {
|
|
|
5229
5381
|
|
|
5230
5382
|
const [publishRouteMutations, observeRouteMutations] = createPubSub();
|
|
5231
5383
|
|
|
5384
|
+
let redirectingRouteSet = null;
|
|
5385
|
+
/**
|
|
5386
|
+
* Where does this url really lead?
|
|
5387
|
+
*
|
|
5388
|
+
* Asked at the door of every navigation, before the url is written anywhere —
|
|
5389
|
+
* an address that only sends elsewhere must not become a page, an entry in the
|
|
5390
|
+
* history, or a route anything can see matching. Answered without reading a
|
|
5391
|
+
* single signal: the url alone says it.
|
|
5392
|
+
*
|
|
5393
|
+
* The chain is followed here rather than by letting each redirection navigate
|
|
5394
|
+
* in turn, so one navigation happens and the addresses in between leave no
|
|
5395
|
+
* trace at all.
|
|
5396
|
+
*
|
|
5397
|
+
* @param {string} url
|
|
5398
|
+
* @returns {string|null} The url to go to instead, or null.
|
|
5399
|
+
*/
|
|
5400
|
+
const resolveRouteRedirection = (url) => {
|
|
5401
|
+
if (!redirectingRouteSet || redirectingRouteSet.size === 0) {
|
|
5402
|
+
return null;
|
|
5403
|
+
}
|
|
5404
|
+
let urlToResolve = url;
|
|
5405
|
+
let redirectionUrl = null;
|
|
5406
|
+
const urlChain = [url];
|
|
5407
|
+
while (true) {
|
|
5408
|
+
const nextUrl = resolveRedirectionOnce(urlToResolve);
|
|
5409
|
+
if (!nextUrl || nextUrl === urlToResolve) {
|
|
5410
|
+
break;
|
|
5411
|
+
}
|
|
5412
|
+
if (urlChain.includes(nextUrl)) {
|
|
5413
|
+
throw new Error(
|
|
5414
|
+
`Redirection cycle: ${[...urlChain, nextUrl].join(" -> ")}`,
|
|
5415
|
+
);
|
|
5416
|
+
}
|
|
5417
|
+
urlChain.push(nextUrl);
|
|
5418
|
+
redirectionUrl = nextUrl;
|
|
5419
|
+
urlToResolve = nextUrl;
|
|
5420
|
+
}
|
|
5421
|
+
return redirectionUrl;
|
|
5422
|
+
};
|
|
5423
|
+
const resolveRedirectionOnce = (url) => {
|
|
5424
|
+
let redirectingRoute = null;
|
|
5425
|
+
let redirectingRouteParams = null;
|
|
5426
|
+
let maxDepth = -1;
|
|
5427
|
+
for (const route of redirectingRouteSet) {
|
|
5428
|
+
const { routePattern } = getRoutePrivateProperties(route);
|
|
5429
|
+
// exact: what a trailing slash catches is what a container renders, not
|
|
5430
|
+
// what an address redirects — "/" redirecting must not take "/cgu" with it
|
|
5431
|
+
const urlParams = routePattern.applyOn(url, { exact: true });
|
|
5432
|
+
if (!urlParams) {
|
|
5433
|
+
continue;
|
|
5434
|
+
}
|
|
5435
|
+
// Deeper = more specific, the reading the whole router shares (see
|
|
5436
|
+
// replaceParams): "/:gameId/invite" is a child of "/:gameId/:shareState",
|
|
5437
|
+
// so it answers for an url both of them match.
|
|
5438
|
+
if (routePattern.depth > maxDepth) {
|
|
5439
|
+
maxDepth = routePattern.depth;
|
|
5440
|
+
redirectingRoute = route;
|
|
5441
|
+
redirectingRouteParams = urlParams;
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
if (!redirectingRoute) {
|
|
5445
|
+
return null;
|
|
5446
|
+
}
|
|
5447
|
+
return buildRedirectionUrl(redirectingRoute, redirectingRouteParams);
|
|
5448
|
+
};
|
|
5449
|
+
const buildRedirectionUrl = (route, urlParams) => {
|
|
5450
|
+
const { redirectRoute, redirectRouteParams } =
|
|
5451
|
+
getRoutePrivateProperties(route);
|
|
5452
|
+
const paramsCarriedOver =
|
|
5453
|
+
redirectRouteParams === null
|
|
5454
|
+
? {}
|
|
5455
|
+
: paramsTargetCanPlace(redirectRoute, urlParams);
|
|
5456
|
+
if (!redirectRouteParams) {
|
|
5457
|
+
return redirectRoute.buildUrl(paramsCarriedOver);
|
|
5458
|
+
}
|
|
5459
|
+
const paramsWritten =
|
|
5460
|
+
typeof redirectRouteParams === "function"
|
|
5461
|
+
? redirectRouteParams(urlParams)
|
|
5462
|
+
: redirectRouteParams;
|
|
5463
|
+
// A param written as undefined is a param dropped: `paramName in params` is
|
|
5464
|
+
// what tells a url build "this one is decided", value included (resolveParams)
|
|
5465
|
+
return redirectRoute.buildUrl({ ...paramsCarriedOver, ...paramsWritten });
|
|
5466
|
+
};
|
|
5467
|
+
|
|
5468
|
+
/**
|
|
5469
|
+
* The params of the url being left that the target route can put somewhere:
|
|
5470
|
+
* its own path segments and the search params it declares. What it cannot
|
|
5471
|
+
* place would otherwise be appended to the url as a query param, and a share
|
|
5472
|
+
* link resolving to "/W-ABC234PQ?shareState=1" is not the address anyone meant.
|
|
5473
|
+
*/
|
|
5474
|
+
const paramsTargetCanPlace = (redirectRoute, urlParams) => {
|
|
5475
|
+
const { routePattern } = getRoutePrivateProperties(redirectRoute);
|
|
5476
|
+
const { parsedPattern, queryConnectionMap } = routePattern;
|
|
5477
|
+
const paramsCarriedOver = {};
|
|
5478
|
+
for (const paramName of Object.keys(urlParams)) {
|
|
5479
|
+
const canPlace =
|
|
5480
|
+
queryConnectionMap.has(paramName) ||
|
|
5481
|
+
parsedPattern.segments.some(
|
|
5482
|
+
(segment) => segment.type === "param" && segment.name === paramName,
|
|
5483
|
+
);
|
|
5484
|
+
if (canPlace) {
|
|
5485
|
+
paramsCarriedOver[paramName] = urlParams[paramName];
|
|
5486
|
+
}
|
|
5487
|
+
}
|
|
5488
|
+
return paramsCarriedOver;
|
|
5489
|
+
};
|
|
5490
|
+
|
|
5232
5491
|
let setupRoutesCalled = false;
|
|
5233
5492
|
const setupRoutes = (routes) => {
|
|
5234
5493
|
if (setupRoutesCalled) {
|
|
@@ -5258,6 +5517,22 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
5258
5517
|
setup({ routeSet, getUrl });
|
|
5259
5518
|
}
|
|
5260
5519
|
|
|
5520
|
+
// Checked here rather than at declaration: a route may redirect to one
|
|
5521
|
+
// declared after it, and reading the target then would forbid that order.
|
|
5522
|
+
redirectingRouteSet = new Set();
|
|
5523
|
+
for (const route of routeSet) {
|
|
5524
|
+
const { redirectRoute } = getRoutePrivateProperties(route);
|
|
5525
|
+
if (!redirectRoute) {
|
|
5526
|
+
continue;
|
|
5527
|
+
}
|
|
5528
|
+
if (!redirectRoute.isRoute) {
|
|
5529
|
+
throw new TypeError(
|
|
5530
|
+
`${route} redirects to ${redirectRoute}, expecting a route object`,
|
|
5531
|
+
);
|
|
5532
|
+
}
|
|
5533
|
+
redirectingRouteSet.add(route);
|
|
5534
|
+
}
|
|
5535
|
+
|
|
5261
5536
|
// Store previous route states to detect changes
|
|
5262
5537
|
const routePreviousStateMap = new WeakMap();
|
|
5263
5538
|
const updateRoutes = (
|
|
@@ -5501,6 +5776,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
5501
5776
|
routePrivatePropertiesMap.delete(route);
|
|
5502
5777
|
}
|
|
5503
5778
|
routeSet.clear();
|
|
5779
|
+
redirectingRouteSet = null;
|
|
5504
5780
|
setupRoutesCalled = false;
|
|
5505
5781
|
};
|
|
5506
5782
|
return {
|
|
@@ -9711,7 +9987,7 @@ const formatDateIso = (iso, inputType) => {
|
|
|
9711
9987
|
const READONLY_CONSTRAINT = {
|
|
9712
9988
|
name: "readonly",
|
|
9713
9989
|
messageAttribute: "data-readonly-message",
|
|
9714
|
-
check: (field) => {
|
|
9990
|
+
check: (field, { intent } = {}) => {
|
|
9715
9991
|
const readOnly = Boolean(
|
|
9716
9992
|
field.controlHostProps.readOnly ||
|
|
9717
9993
|
field.controlHostProps["aria-readonly"] === "true",
|
|
@@ -9720,6 +9996,18 @@ const READONLY_CONSTRAINT = {
|
|
|
9720
9996
|
return null;
|
|
9721
9997
|
}
|
|
9722
9998
|
|
|
9999
|
+
// Read-only, and what it opens still opens: a picker's answer often lives
|
|
10000
|
+
// in a shape only its popup draws — a plan with one tile ringed, a wheel
|
|
10001
|
+
// stopped on a time — and refusing to open leaves that shape unreadable.
|
|
10002
|
+
// Opening reads and nothing more, so it goes through; everything that would
|
|
10003
|
+
// write is refused below, and the popup content is handed the same
|
|
10004
|
+
// read-only state so each control in there refuses on its own terms. Which
|
|
10005
|
+
// controls say so, and when, is theirs to answer (see createControlInfo's
|
|
10006
|
+
// readOnlyOpens).
|
|
10007
|
+
if (intent === "read" && field.readOnlyOpens) {
|
|
10008
|
+
return null;
|
|
10009
|
+
}
|
|
10010
|
+
|
|
9723
10011
|
// A selection guarding its length (see maxLengthGuard) is what holds this
|
|
9724
10012
|
// one back, so max_length is what refuses it: same name, same message, same
|
|
9725
10013
|
// `maxLengthMessage` to say it in the caller's own words. Read-only is only
|
|
@@ -9784,6 +10072,8 @@ const readOnlyMessage = (field) => {
|
|
|
9784
10072
|
* → "navi_request_interaction" event
|
|
9785
10073
|
* → onRequestInteraction
|
|
9786
10074
|
* → check disabled / read-only / busy (via controller.controlInteraction)
|
|
10075
|
+
* against the interaction's `intent` ("write" by default, "read" for one
|
|
10076
|
+
* that only shows what is already there — see READONLY_CONSTRAINT)
|
|
9787
10077
|
* → if blocked → prevented()
|
|
9788
10078
|
* → if allowed → allowed()
|
|
9789
10079
|
* → (in allowed callback) setUIState(value)
|
|
@@ -9817,10 +10107,10 @@ const createControlInteraction = (
|
|
|
9817
10107
|
// The title this rule put on the element, if any (see checkInteractivity).
|
|
9818
10108
|
let titleWritten = null;
|
|
9819
10109
|
|
|
9820
|
-
const checkInteractivity = ({ event } = {}) => {
|
|
10110
|
+
const checkInteractivity = ({ event, intent = "write" } = {}) => {
|
|
9821
10111
|
interactionFailedConstraintInfo = null;
|
|
9822
10112
|
for (const constraint of INTERACTION_CONSTRAINT_SET) {
|
|
9823
|
-
const checkResult = constraint.check(controller);
|
|
10113
|
+
const checkResult = constraint.check(controller, { intent });
|
|
9824
10114
|
if (!checkResult) {
|
|
9825
10115
|
continue;
|
|
9826
10116
|
}
|
|
@@ -9847,7 +10137,7 @@ const createControlInteraction = (
|
|
|
9847
10137
|
if (!mci) {
|
|
9848
10138
|
continue;
|
|
9849
10139
|
}
|
|
9850
|
-
const canInteract = mci.checkInteractivity({ event });
|
|
10140
|
+
const canInteract = mci.checkInteractivity({ event, intent });
|
|
9851
10141
|
if (canInteract) {
|
|
9852
10142
|
continue;
|
|
9853
10143
|
}
|
|
@@ -9861,8 +10151,14 @@ const createControlInteraction = (
|
|
|
9861
10151
|
}
|
|
9862
10152
|
|
|
9863
10153
|
// Keep title attribute in sync for accessibility.
|
|
10154
|
+
// Only off a check that asked the general question: a title is read
|
|
10155
|
+
// whenever a pointer rests on the element, so it says what is true of the
|
|
10156
|
+
// control as a whole. A check made for an interaction that only reads (a
|
|
10157
|
+
// read-only picker asked to open its popup) answers about that one
|
|
10158
|
+
// interaction — writing the title from it would take away the "read-only"
|
|
10159
|
+
// the first time someone opened the popup.
|
|
9864
10160
|
const titleLess = !controller.controlHostProps?.title;
|
|
9865
|
-
if (titleLess) {
|
|
10161
|
+
if (intent === "write" && titleLess) {
|
|
9866
10162
|
const element = controller.ref.current;
|
|
9867
10163
|
if (element) {
|
|
9868
10164
|
if (interactionFailedConstraintInfo) {
|
|
@@ -9968,6 +10264,12 @@ const onRequestInteraction = (
|
|
|
9968
10264
|
const {
|
|
9969
10265
|
event,
|
|
9970
10266
|
name,
|
|
10267
|
+
// What this interaction would do to the control: write it, or only read it.
|
|
10268
|
+
// Everything writes unless it says otherwise — an interaction that merely
|
|
10269
|
+
// shows what is already there (opening a picker's popup, closing it again)
|
|
10270
|
+
// says "read", and that is what a control held read-only can still let
|
|
10271
|
+
// through (see READONLY_CONSTRAINT).
|
|
10272
|
+
intent = "write",
|
|
9971
10273
|
bypassInteractivity = false,
|
|
9972
10274
|
prevented,
|
|
9973
10275
|
allowed,
|
|
@@ -9998,7 +10300,7 @@ const onRequestInteraction = (
|
|
|
9998
10300
|
if (controller && !bypassInteractivity) {
|
|
9999
10301
|
const ci = controller?.rules.interaction;
|
|
10000
10302
|
if (ci) {
|
|
10001
|
-
const canInteract = ci.checkInteractivity({ event });
|
|
10303
|
+
const canInteract = ci.checkInteractivity({ event, intent });
|
|
10002
10304
|
if (!canInteract) {
|
|
10003
10305
|
const failedInfo =
|
|
10004
10306
|
ci.interactionFailedConstraintInfo ??
|
|
@@ -21522,6 +21824,24 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
21522
21824
|
// the single door every navigation goes through, rather than by each caller
|
|
21523
21825
|
// (navBack's fallback in particular arrives here raw).
|
|
21524
21826
|
const url = new URL(target, window.location.href).href;
|
|
21827
|
+
// An address that only sends elsewhere never becomes anything here: asked
|
|
21828
|
+
// before the announcement, before the history write and before the routes,
|
|
21829
|
+
// so that what follows is entirely about where the reader is going (see
|
|
21830
|
+
// resolveRouteRedirection).
|
|
21831
|
+
const redirectionUrl = resolveRouteRedirection(url);
|
|
21832
|
+
if (redirectionUrl) {
|
|
21833
|
+
if (redirectionUrl === window.location.href) {
|
|
21834
|
+
// Asked to go where we already are: an address that redirects is never
|
|
21835
|
+
// the one being displayed, so there is nothing to go to and nothing to
|
|
21836
|
+
// stack on the history.
|
|
21837
|
+
return undefined;
|
|
21838
|
+
}
|
|
21839
|
+
return handleRoutingTask(redirectionUrl, {
|
|
21840
|
+
...options,
|
|
21841
|
+
reason: `${options.reason} (redirected from ${url})`,
|
|
21842
|
+
redirected: true,
|
|
21843
|
+
});
|
|
21844
|
+
}
|
|
21525
21845
|
// Decided before anything is announced: an elided push IS the traversal it
|
|
21526
21846
|
// becomes, and the traversal will make its own announcements when the
|
|
21527
21847
|
// browser answers — a before/after cycle here would be about a navigation
|
|
@@ -21570,6 +21890,7 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
21570
21890
|
reason,
|
|
21571
21891
|
navigationType, // "load", "reload", "replace", "push", "traverse"
|
|
21572
21892
|
state,
|
|
21893
|
+
redirected,
|
|
21573
21894
|
} = options;
|
|
21574
21895
|
|
|
21575
21896
|
// Where the entry being reached stands in this document's own stack —
|
|
@@ -21612,6 +21933,14 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
21612
21933
|
} else {
|
|
21613
21934
|
// traverse / reload: state comes from the history entry, no push/replace needed.
|
|
21614
21935
|
markUrlAsVisited(url);
|
|
21936
|
+
if (redirected) {
|
|
21937
|
+
// The entry the browser is on names an address that only sends
|
|
21938
|
+
// elsewhere — a cold load on it, or a back into it. Written over where
|
|
21939
|
+
// it stands (the entry keeps its place in the stack, hence its state
|
|
21940
|
+
// and the depth in it): pressing back must not walk into it again.
|
|
21941
|
+
window.history.replaceState(state, null, url);
|
|
21942
|
+
rememberEntryIsOfThisDocument();
|
|
21943
|
+
}
|
|
21615
21944
|
updateDocumentUrl(url);
|
|
21616
21945
|
updateDocumentState(state);
|
|
21617
21946
|
}
|
|
@@ -26412,6 +26741,7 @@ const useControlProps = (props, {
|
|
|
26412
26741
|
}
|
|
26413
26742
|
const {
|
|
26414
26743
|
name,
|
|
26744
|
+
intent,
|
|
26415
26745
|
bypassInteractivity = false,
|
|
26416
26746
|
allowed,
|
|
26417
26747
|
prevented,
|
|
@@ -26421,6 +26751,7 @@ const useControlProps = (props, {
|
|
|
26421
26751
|
return dispatchRequestInteraction(control, {
|
|
26422
26752
|
event: e,
|
|
26423
26753
|
name,
|
|
26754
|
+
intent,
|
|
26424
26755
|
bypassInteractivity,
|
|
26425
26756
|
prevented: () => {
|
|
26426
26757
|
debugInteraction(e, `interaction not allowed`);
|
|
@@ -26560,6 +26891,7 @@ const createControlInfo = (props, {
|
|
|
26560
26891
|
let defaultStatePropName;
|
|
26561
26892
|
let stateInitial;
|
|
26562
26893
|
let readOnlySupported = false;
|
|
26894
|
+
let readOnlyOpens = false;
|
|
26563
26895
|
let disabledSupported = false;
|
|
26564
26896
|
let hasStateProp;
|
|
26565
26897
|
let value;
|
|
@@ -26646,6 +26978,14 @@ const createControlInfo = (props, {
|
|
|
26646
26978
|
// aria-readonly plus a refused interaction — see the select reactions in
|
|
26647
26979
|
// getDefaultEventReactionDefinitions.
|
|
26648
26980
|
readOnlySupported = controlType === "picker" && INPUT_TYPE_SUPPORTING_READONLY_SET.has(typeProp);
|
|
26981
|
+
// A picker's popup is content of its own — a plan with one tile ringed, a
|
|
26982
|
+
// wheel stopped on a time, a list showing what was chosen — so read-only
|
|
26983
|
+
// does not close it: it opens, and everything in it is held read-only in
|
|
26984
|
+
// turn (see the ReadOnlyContext in picker.jsx). Two pickers this is not
|
|
26985
|
+
// true of: one with no popup of its own, which opens the browser's and
|
|
26986
|
+
// cannot hold that read-only (see PickerNative), and one whose caller says
|
|
26987
|
+
// its popup is a form with nothing to read (openWhileReadOnly={false}).
|
|
26988
|
+
readOnlyOpens = controlType === "picker" && props.children !== undefined && props.openWhileReadOnly !== false;
|
|
26649
26989
|
}
|
|
26650
26990
|
|
|
26651
26991
|
// The suggestion the control starts on, as opposed to what it holds — what a
|
|
@@ -26672,6 +27012,7 @@ const createControlInfo = (props, {
|
|
|
26672
27012
|
signalHoldsChecked,
|
|
26673
27013
|
stateFromSignal,
|
|
26674
27014
|
readOnlySupported,
|
|
27015
|
+
readOnlyOpens,
|
|
26675
27016
|
disabledSupported
|
|
26676
27017
|
};
|
|
26677
27018
|
};
|
|
@@ -27022,6 +27363,10 @@ const useInteractiveProps = (props, {
|
|
|
27022
27363
|
const actionLoading = optimistic ? false : actionStatus.loading;
|
|
27023
27364
|
const loadingResolved = loadingBase || actionLoading;
|
|
27024
27365
|
const readOnlyResolved = readOnlyBase || actionLoading;
|
|
27366
|
+
// Read-only, and what this control opens still opens: reading what is in
|
|
27367
|
+
// there changes nothing. Read by READONLY_CONSTRAINT, which lets an
|
|
27368
|
+
// interaction that only reads through on it.
|
|
27369
|
+
uiStateController.readOnlyOpens = Boolean(controlInfo.readOnlyOpens);
|
|
27025
27370
|
// Both halves of "busy" that do not come from the bound action, kept apart
|
|
27026
27371
|
// from each other and from it: BUSY_CONSTRAINT answers each from its own
|
|
27027
27372
|
// live source rather than from the rendered aria-busy, which conflates all
|
|
@@ -47385,7 +47730,27 @@ installImportMetaCssBuild(import.meta);const css$J = /* css */`
|
|
|
47385
47730
|
border-width: var(--border-width);
|
|
47386
47731
|
border-style: solid;
|
|
47387
47732
|
border-color: var(--x-border-color);
|
|
47388
|
-
|
|
47733
|
+
/* Squared from the outside, corner by corner: a checkbox in a group can
|
|
47734
|
+
arrive wrapped (in a label, in a row carrying a state), and the ask then
|
|
47735
|
+
travels down as inherited custom properties rather than as a radius
|
|
47736
|
+
landing on this element. Each corner falls back to the checkbox's own
|
|
47737
|
+
radius when nothing asks for anything. */
|
|
47738
|
+
border-top-left-radius: var(
|
|
47739
|
+
--x-corner-top-left-radius,
|
|
47740
|
+
var(--border-radius)
|
|
47741
|
+
);
|
|
47742
|
+
border-top-right-radius: var(
|
|
47743
|
+
--x-corner-top-right-radius,
|
|
47744
|
+
var(--border-radius)
|
|
47745
|
+
);
|
|
47746
|
+
border-bottom-right-radius: var(
|
|
47747
|
+
--x-corner-bottom-right-radius,
|
|
47748
|
+
var(--border-radius)
|
|
47749
|
+
);
|
|
47750
|
+
border-bottom-left-radius: var(
|
|
47751
|
+
--x-corner-bottom-left-radius,
|
|
47752
|
+
var(--border-radius)
|
|
47753
|
+
);
|
|
47389
47754
|
outline-width: var(--outline-width);
|
|
47390
47755
|
outline-style: none;
|
|
47391
47756
|
outline-color: var(--outline-color);
|
|
@@ -48351,7 +48716,26 @@ installImportMetaCssBuild(import.meta);const css$H = /* css */`
|
|
|
48351
48716
|
border-width: var(--button-border-width);
|
|
48352
48717
|
border-style: solid;
|
|
48353
48718
|
border-color: var(--x-border-color);
|
|
48354
|
-
|
|
48719
|
+
/* Squared from the outside, corner by corner: a row of these is the
|
|
48720
|
+
segmented control a Group is for, and one of them can arrive wrapped
|
|
48721
|
+
(in a tooltip, in a label) — so the ask travels down as inherited
|
|
48722
|
+
custom properties rather than as a radius landing on this element. */
|
|
48723
|
+
border-top-left-radius: var(
|
|
48724
|
+
--x-corner-top-left-radius,
|
|
48725
|
+
var(--button-border-radius)
|
|
48726
|
+
);
|
|
48727
|
+
border-top-right-radius: var(
|
|
48728
|
+
--x-corner-top-right-radius,
|
|
48729
|
+
var(--button-border-radius)
|
|
48730
|
+
);
|
|
48731
|
+
border-bottom-right-radius: var(
|
|
48732
|
+
--x-corner-bottom-right-radius,
|
|
48733
|
+
var(--button-border-radius)
|
|
48734
|
+
);
|
|
48735
|
+
border-bottom-left-radius: var(
|
|
48736
|
+
--x-corner-bottom-left-radius,
|
|
48737
|
+
var(--button-border-radius)
|
|
48738
|
+
);
|
|
48355
48739
|
|
|
48356
48740
|
.navi_icon,
|
|
48357
48741
|
img {
|
|
@@ -48660,6 +49044,28 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
|
|
|
48660
49044
|
--x-thumb-color: var(--thumb-color);
|
|
48661
49045
|
--x-thumb-border: none;
|
|
48662
49046
|
--x-thumb-cursor: var(--thumb-cursor);
|
|
49047
|
+
/* Squared from the outside, corner by corner — resolved here once because
|
|
49048
|
+
what draws the frame is not this element but the three layers stacked
|
|
49049
|
+
below it (background, track, fill), which cannot take it by inherit the
|
|
49050
|
+
way a control with a single frame does. A range can arrive wrapped, so
|
|
49051
|
+
the ask travels down as inherited custom properties; each corner falls
|
|
49052
|
+
back to the track's own radius when nothing asks for anything. */
|
|
49053
|
+
--x-range-corner-top-left: var(
|
|
49054
|
+
--x-corner-top-left-radius,
|
|
49055
|
+
var(--border-radius)
|
|
49056
|
+
);
|
|
49057
|
+
--x-range-corner-top-right: var(
|
|
49058
|
+
--x-corner-top-right-radius,
|
|
49059
|
+
var(--border-radius)
|
|
49060
|
+
);
|
|
49061
|
+
--x-range-corner-bottom-right: var(
|
|
49062
|
+
--x-corner-bottom-right-radius,
|
|
49063
|
+
var(--border-radius)
|
|
49064
|
+
);
|
|
49065
|
+
--x-range-corner-bottom-left: var(
|
|
49066
|
+
--x-corner-bottom-left-radius,
|
|
49067
|
+
var(--border-radius)
|
|
49068
|
+
);
|
|
48663
49069
|
|
|
48664
49070
|
position: relative;
|
|
48665
49071
|
box-sizing: border-box;
|
|
@@ -48668,10 +49074,26 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
|
|
|
48668
49074
|
margin: 2px;
|
|
48669
49075
|
flex-direction: row;
|
|
48670
49076
|
align-items: center;
|
|
48671
|
-
/* Just for the outline, the real border radius of the range is fixed */
|
|
48672
49077
|
font-size: var(--font-size);
|
|
48673
49078
|
font-family: var(--font-family);
|
|
48674
|
-
|
|
49079
|
+
/* The ring around the whole control, which follows the claim too: a range
|
|
49080
|
+
squared along a seam must not keep a rounded ring over it. */
|
|
49081
|
+
border-top-left-radius: var(
|
|
49082
|
+
--x-corner-top-left-radius,
|
|
49083
|
+
var(--outline-border-radius)
|
|
49084
|
+
);
|
|
49085
|
+
border-top-right-radius: var(
|
|
49086
|
+
--x-corner-top-right-radius,
|
|
49087
|
+
var(--outline-border-radius)
|
|
49088
|
+
);
|
|
49089
|
+
border-bottom-right-radius: var(
|
|
49090
|
+
--x-corner-bottom-right-radius,
|
|
49091
|
+
var(--outline-border-radius)
|
|
49092
|
+
);
|
|
49093
|
+
border-bottom-left-radius: var(
|
|
49094
|
+
--x-corner-bottom-left-radius,
|
|
49095
|
+
var(--outline-border-radius)
|
|
49096
|
+
);
|
|
48675
49097
|
outline-width: var(--outline-width);
|
|
48676
49098
|
outline-style: none;
|
|
48677
49099
|
outline-color: var(--outline-color);
|
|
@@ -48712,7 +49134,10 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
|
|
|
48712
49134
|
border-width: var(--border-width);
|
|
48713
49135
|
border-style: solid;
|
|
48714
49136
|
border-color: var(--x-border-color);
|
|
48715
|
-
border-radius: var(--
|
|
49137
|
+
border-top-left-radius: var(--x-range-corner-top-left);
|
|
49138
|
+
border-top-right-radius: var(--x-range-corner-top-right);
|
|
49139
|
+
border-bottom-right-radius: var(--x-range-corner-bottom-right);
|
|
49140
|
+
border-bottom-left-radius: var(--x-range-corner-bottom-left);
|
|
48716
49141
|
}
|
|
48717
49142
|
.navi_input_range_track {
|
|
48718
49143
|
position: absolute;
|
|
@@ -48722,7 +49147,10 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
|
|
|
48722
49147
|
border-width: var(--border-width);
|
|
48723
49148
|
border-style: solid;
|
|
48724
49149
|
border-color: var(--x-track-border-color);
|
|
48725
|
-
border-radius: var(--
|
|
49150
|
+
border-top-left-radius: var(--x-range-corner-top-left);
|
|
49151
|
+
border-top-right-radius: var(--x-range-corner-top-right);
|
|
49152
|
+
border-bottom-right-radius: var(--x-range-corner-bottom-right);
|
|
49153
|
+
border-bottom-left-radius: var(--x-range-corner-bottom-left);
|
|
48726
49154
|
}
|
|
48727
49155
|
.navi_input_range_fill {
|
|
48728
49156
|
position: absolute;
|
|
@@ -48730,7 +49158,10 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
|
|
|
48730
49158
|
height: var(--height);
|
|
48731
49159
|
background: var(--x-fill-color);
|
|
48732
49160
|
background-clip: content-box;
|
|
48733
|
-
border-radius: var(--
|
|
49161
|
+
border-top-left-radius: var(--x-range-corner-top-left);
|
|
49162
|
+
border-top-right-radius: var(--x-range-corner-top-right);
|
|
49163
|
+
border-bottom-right-radius: var(--x-range-corner-bottom-right);
|
|
49164
|
+
border-bottom-left-radius: var(--x-range-corner-bottom-left);
|
|
48734
49165
|
clip-path: inset(0 calc((1 - var(--x-fill-ratio)) * 100%) 0 0);
|
|
48735
49166
|
}
|
|
48736
49167
|
.navi_input_range_thumb {
|
|
@@ -49802,7 +50233,27 @@ const inputCss = /* css */`
|
|
|
49802
50233
|
border-width: var(--border-width);
|
|
49803
50234
|
border-style: solid;
|
|
49804
50235
|
border-color: var(--x-border-color);
|
|
49805
|
-
|
|
50236
|
+
/* Squared from the outside, corner by corner: an input is not always the
|
|
50237
|
+
member a Group joins — it can arrive wrapped (in a Box carrying a state,
|
|
50238
|
+
in a tooltip) — so the ask travels down as inherited custom properties
|
|
50239
|
+
rather than as a radius landing on this element. Each corner falls back
|
|
50240
|
+
to the input's own radius when nothing asks for anything. */
|
|
50241
|
+
border-top-left-radius: var(
|
|
50242
|
+
--x-corner-top-left-radius,
|
|
50243
|
+
var(--border-radius)
|
|
50244
|
+
);
|
|
50245
|
+
border-top-right-radius: var(
|
|
50246
|
+
--x-corner-top-right-radius,
|
|
50247
|
+
var(--border-radius)
|
|
50248
|
+
);
|
|
50249
|
+
border-bottom-right-radius: var(
|
|
50250
|
+
--x-corner-bottom-right-radius,
|
|
50251
|
+
var(--border-radius)
|
|
50252
|
+
);
|
|
50253
|
+
border-bottom-left-radius: var(
|
|
50254
|
+
--x-corner-bottom-left-radius,
|
|
50255
|
+
var(--border-radius)
|
|
50256
|
+
);
|
|
49806
50257
|
outline-width: var(--outline-width);
|
|
49807
50258
|
outline-color: var(--outline-color);
|
|
49808
50259
|
outline-offset: var(--outline-offset);
|
|
@@ -51543,7 +51994,7 @@ const css$E = /* css */`
|
|
|
51543
51994
|
positioned element to mean anything, hence position: relative.
|
|
51544
51995
|
Deliberately not paired with isolation: isolate — a stacking context
|
|
51545
51996
|
here would also trap the popup of a picker held in the group, which
|
|
51546
|
-
counts on its own band reaching the whole page. What keeps these
|
|
51997
|
+
counts on its own band reaching the whole page. What keeps these
|
|
51547
51998
|
values from escaping is instead that everything they could reach is a
|
|
51548
51999
|
band above them (see navi_z_indexes.js). */
|
|
51549
52000
|
> *:hover,
|
|
@@ -51563,6 +52014,25 @@ const css$E = /* css */`
|
|
|
51563
52014
|
position: relative;
|
|
51564
52015
|
z-index: var(--navi-z-index-control-focused);
|
|
51565
52016
|
}
|
|
52017
|
+
/* The member holding something open. Neither of the two above covers it:
|
|
52018
|
+
the click that opened the popup gives no focus ring, the focus itself
|
|
52019
|
+
left for the popup's content, and the pointer is free to travel to a
|
|
52020
|
+
neighbour — yet the member keeps the border color its open state gives
|
|
52021
|
+
it, and that border is exactly what the neighbour painted after it
|
|
52022
|
+
slices. Read as a state, not as a pseudo-class: :active only lasts as
|
|
52023
|
+
long as the button is held down, and while it is held :hover is true
|
|
52024
|
+
anyway, so it would add nothing here.
|
|
52025
|
+
|
|
52026
|
+
:has, for the same reason as focus-visible above — the group member can
|
|
52027
|
+
be an enrobage around the control that expands — and reaching a popup
|
|
52028
|
+
held inline (a Popover with layer="local" renders inside its member)
|
|
52029
|
+
costs nothing: that popup only reads expanded while its own member is,
|
|
52030
|
+
which is the member this raises. */
|
|
52031
|
+
> *[aria-expanded="true"],
|
|
52032
|
+
> *:has([aria-expanded="true"]) {
|
|
52033
|
+
position: relative;
|
|
52034
|
+
z-index: var(--navi-z-index-control-expanded);
|
|
52035
|
+
}
|
|
51566
52036
|
|
|
51567
52037
|
/* Horizontal (default): Cumulative margin for border overlap */
|
|
51568
52038
|
&:not([data-vertical]) {
|
|
@@ -55490,6 +55960,14 @@ const PickerNative = props => {
|
|
|
55490
55960
|
dispatchRequestInteraction(pickerInput, {
|
|
55491
55961
|
event: e,
|
|
55492
55962
|
name: "navi_request_open to show native picker",
|
|
55963
|
+
// No "read" intent here, unlike a picker holding a popup of its own
|
|
55964
|
+
// (see PickerCustom): the browser's picker cannot be held read-only,
|
|
55965
|
+
// whichever way its type falls. Where `readonly` applies (date, time,
|
|
55966
|
+
// month, number…) the input is not mutable and showPicker() refuses
|
|
55967
|
+
// it — there is nothing to open. Where it does not (color, file) the
|
|
55968
|
+
// browser opens all the same and writes whatever is chosen straight
|
|
55969
|
+
// into the input, which is read-only in name only. So a read-only
|
|
55970
|
+
// native picker says why instead, on the trigger.
|
|
55493
55971
|
prevented: () => {
|
|
55494
55972
|
e.preventDefault();
|
|
55495
55973
|
},
|
|
@@ -55680,14 +56158,20 @@ const PickerCustom = props => {
|
|
|
55680
56158
|
// already what it is, and this only re-runs the same reaction.
|
|
55681
56159
|
const inputEl = getPickerInput(ref.current);
|
|
55682
56160
|
const valueAtClose = getUIStateFromElement(inputEl);
|
|
55683
|
-
|
|
56161
|
+
const controller = inputEl?.__uiStateController__;
|
|
56162
|
+
if (controller?.controlHostProps.readOnly) {
|
|
56163
|
+
// Opened only to be read: what it shows stays the suggestion it
|
|
56164
|
+
// was. A look is not an answer, and the signal behind it is not
|
|
56165
|
+
// written by one.
|
|
56166
|
+
debugPopup(closeEvent, `picker is read-only -> nothing to commit`);
|
|
56167
|
+
} else if (valueAtOpen === undefined && compareTwoJsValues(valueAtClose, valueAtOpen)) {
|
|
55684
56168
|
// Same third case onRequestClose steps around: nothing held,
|
|
55685
56169
|
// nothing shown, nothing picked. There is no suggestion here to
|
|
55686
56170
|
// turn into an answer.
|
|
55687
56171
|
debugPopup(closeEvent, `picker showed nothing -> nothing to commit`);
|
|
55688
56172
|
} else {
|
|
55689
56173
|
debugPopup(closeEvent, `picker defined a suggestion -> commit it`);
|
|
55690
|
-
commitUIStateAsAnswer(
|
|
56174
|
+
commitUIStateAsAnswer(controller, closeEvent);
|
|
55691
56175
|
}
|
|
55692
56176
|
}
|
|
55693
56177
|
leaveExpanded({
|
|
@@ -55776,6 +56260,13 @@ const PickerCustom = props => {
|
|
|
55776
56260
|
requestInteraction({
|
|
55777
56261
|
event: e,
|
|
55778
56262
|
name: "navi_request_open_event",
|
|
56263
|
+
// Showing what the picker already holds, in the shape only the popup
|
|
56264
|
+
// draws it in — nothing of the value is written on the way in, nor on
|
|
56265
|
+
// the way out. Every interaction below says the same, which is what
|
|
56266
|
+
// lets a read-only picker be opened and read while everything that
|
|
56267
|
+
// would write it (paste, cut, the clear cross) stays refused. See
|
|
56268
|
+
// READONLY_CONSTRAINT.
|
|
56269
|
+
intent: "read",
|
|
55779
56270
|
allowed: () => {
|
|
55780
56271
|
requestOpen(e);
|
|
55781
56272
|
}
|
|
@@ -55784,6 +56275,7 @@ const PickerCustom = props => {
|
|
|
55784
56275
|
onnavi_request_close: e => {
|
|
55785
56276
|
requestInteraction({
|
|
55786
56277
|
event: e,
|
|
56278
|
+
intent: "read",
|
|
55787
56279
|
allowed: () => {
|
|
55788
56280
|
requestClose(e, {
|
|
55789
56281
|
isCancel: e.detail.isCancel
|
|
@@ -55802,6 +56294,7 @@ const PickerCustom = props => {
|
|
|
55802
56294
|
"a-z": e => {
|
|
55803
56295
|
return {
|
|
55804
56296
|
name: "letter key to open",
|
|
56297
|
+
intent: "read",
|
|
55805
56298
|
allowed: () => {
|
|
55806
56299
|
requestOpen(e);
|
|
55807
56300
|
}
|
|
@@ -55810,6 +56303,7 @@ const PickerCustom = props => {
|
|
|
55810
56303
|
"0-9": e => {
|
|
55811
56304
|
return {
|
|
55812
56305
|
name: "numeric key to open",
|
|
56306
|
+
intent: "read",
|
|
55813
56307
|
allowed: () => {
|
|
55814
56308
|
requestOpen(e);
|
|
55815
56309
|
}
|
|
@@ -55818,6 +56312,7 @@ const PickerCustom = props => {
|
|
|
55818
56312
|
"arrowdown": e => {
|
|
55819
56313
|
return {
|
|
55820
56314
|
name: "arrow_down_to_open",
|
|
56315
|
+
intent: "read",
|
|
55821
56316
|
allowed: () => {
|
|
55822
56317
|
requestOpen(e);
|
|
55823
56318
|
e.preventDefault(); // prevent container scroll
|
|
@@ -55827,6 +56322,7 @@ const PickerCustom = props => {
|
|
|
55827
56322
|
"arrowup": e => {
|
|
55828
56323
|
return {
|
|
55829
56324
|
name: "arrow_up_to_open",
|
|
56325
|
+
intent: "read",
|
|
55830
56326
|
allowed: () => {
|
|
55831
56327
|
requestOpen(e);
|
|
55832
56328
|
e.preventDefault(); // prevent container scroll
|
|
@@ -55836,6 +56332,7 @@ const PickerCustom = props => {
|
|
|
55836
56332
|
"space": e => {
|
|
55837
56333
|
return {
|
|
55838
56334
|
name: "space_to_open",
|
|
56335
|
+
intent: "read",
|
|
55839
56336
|
allowed: () => {
|
|
55840
56337
|
requestOpen(e);
|
|
55841
56338
|
e.preventDefault(); // prevent scroll
|
|
@@ -55850,6 +56347,7 @@ const PickerCustom = props => {
|
|
|
55850
56347
|
}
|
|
55851
56348
|
return {
|
|
55852
56349
|
name: "enter_to_open",
|
|
56350
|
+
intent: "read",
|
|
55853
56351
|
allowed: () => {
|
|
55854
56352
|
requestOpen(e);
|
|
55855
56353
|
e.preventDefault(); // prevent form submission
|
|
@@ -55863,6 +56361,7 @@ const PickerCustom = props => {
|
|
|
55863
56361
|
const isCancel = escapeEffect === "cancel";
|
|
55864
56362
|
return {
|
|
55865
56363
|
name: isCancel ? "escape_to_cancel" : "escape_to_close",
|
|
56364
|
+
intent: "read",
|
|
55866
56365
|
allowed: () => {
|
|
55867
56366
|
requestClose(e, {
|
|
55868
56367
|
isCancel
|
|
@@ -55894,6 +56393,7 @@ const PickerCustom = props => {
|
|
|
55894
56393
|
// choice being taken from anyone.
|
|
55895
56394
|
return {
|
|
55896
56395
|
name: "mousedown to close picker",
|
|
56396
|
+
intent: "read",
|
|
55897
56397
|
allowed: () => requestClose(e, {
|
|
55898
56398
|
isCancel: true
|
|
55899
56399
|
})
|
|
@@ -55904,6 +56404,7 @@ const PickerCustom = props => {
|
|
|
55904
56404
|
}
|
|
55905
56405
|
return {
|
|
55906
56406
|
name: "mousedown to open picker",
|
|
56407
|
+
intent: "read",
|
|
55907
56408
|
allowed: () => {
|
|
55908
56409
|
debugFocus(e, `prevent browser giving focus to button (mousedown.preventDefault())`);
|
|
55909
56410
|
requestOpen(e);
|
|
@@ -55921,6 +56422,7 @@ const PickerCustom = props => {
|
|
|
55921
56422
|
// above), this is where the picker opens for real.
|
|
55922
56423
|
return {
|
|
55923
56424
|
name: e.detail === 0 ? "click (keyboard or progammatic) to open picker" : "click to open picker",
|
|
56425
|
+
intent: "read",
|
|
55924
56426
|
prevented: () => {
|
|
55925
56427
|
e.preventDefault();
|
|
55926
56428
|
},
|
|
@@ -62885,10 +63387,39 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
|
|
|
62885
63387
|
/* The frame is drawn by the box, but its radius is declared here, on the
|
|
62886
63388
|
control root, like every other navi control does — so anything styling
|
|
62887
63389
|
the picker from the outside (a Group squaring the corners it joins) has
|
|
62888
|
-
one element to talk to, and the box follows.
|
|
62889
|
-
|
|
63390
|
+
one element to talk to, and the box follows.
|
|
63391
|
+
Corner by corner rather than as the shorthand: a picker is not always
|
|
63392
|
+
the member a Group joins — it can arrive wrapped (in a Box carrying a
|
|
63393
|
+
state, in a link, in a tooltip), and the ask then travels down as
|
|
63394
|
+
inherited custom properties instead of landing on this element as a
|
|
63395
|
+
radius. Each corner falls back to the picker's own radius when nothing
|
|
63396
|
+
asks for anything. */
|
|
63397
|
+
border-top-left-radius: var(
|
|
63398
|
+
--x-corner-top-left-radius,
|
|
63399
|
+
var(--picker-border-radius)
|
|
63400
|
+
);
|
|
63401
|
+
border-top-right-radius: var(
|
|
63402
|
+
--x-corner-top-right-radius,
|
|
63403
|
+
var(--picker-border-radius)
|
|
63404
|
+
);
|
|
63405
|
+
border-bottom-right-radius: var(
|
|
63406
|
+
--x-corner-bottom-right-radius,
|
|
63407
|
+
var(--picker-border-radius)
|
|
63408
|
+
);
|
|
63409
|
+
border-bottom-left-radius: var(
|
|
63410
|
+
--x-corner-bottom-left-radius,
|
|
63411
|
+
var(--picker-border-radius)
|
|
63412
|
+
);
|
|
62890
63413
|
|
|
62891
63414
|
.navi_picker_box {
|
|
63415
|
+
/* The ask stops here: this element is the picker's frame, so nothing it
|
|
63416
|
+
holds is at the seam — the chevron and the clear cross in the slot, a
|
|
63417
|
+
button in a custom UI. */
|
|
63418
|
+
--x-corner-top-left-radius: initial;
|
|
63419
|
+
--x-corner-top-right-radius: initial;
|
|
63420
|
+
--x-corner-bottom-right-radius: initial;
|
|
63421
|
+
--x-corner-bottom-left-radius: initial;
|
|
63422
|
+
|
|
62892
63423
|
position: relative;
|
|
62893
63424
|
display: inline-flex;
|
|
62894
63425
|
box-sizing: border-box;
|
|
@@ -62940,14 +63471,6 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
|
|
|
62940
63471
|
}
|
|
62941
63472
|
}
|
|
62942
63473
|
.navi_picker_right_slot {
|
|
62943
|
-
/* A corner claimed from the outside (see group.jsx) is the frame's, and
|
|
62944
|
-
what lives in here is not the frame — a button in a slot, the content
|
|
62945
|
-
of a popup — so the ask stops at this boundary. */
|
|
62946
|
-
--x-corner-top-left-radius: initial;
|
|
62947
|
-
--x-corner-top-right-radius: initial;
|
|
62948
|
-
--x-corner-bottom-right-radius: initial;
|
|
62949
|
-
--x-corner-bottom-left-radius: initial;
|
|
62950
|
-
|
|
62951
63474
|
display: inline-flex;
|
|
62952
63475
|
height: 1em;
|
|
62953
63476
|
height: 1lh;
|
|
@@ -63042,6 +63565,15 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
|
|
|
63042
63565
|
}
|
|
63043
63566
|
|
|
63044
63567
|
.navi_picker_content {
|
|
63568
|
+
/* The other side of the frame: what a picker holds here is what its
|
|
63569
|
+
popup shows, and a popup is never at a seam. Popover and Dialog stop
|
|
63570
|
+
the ask at their own root too — this covers content that reaches the
|
|
63571
|
+
popup through neither. */
|
|
63572
|
+
--x-corner-top-left-radius: initial;
|
|
63573
|
+
--x-corner-top-right-radius: initial;
|
|
63574
|
+
--x-corner-bottom-right-radius: initial;
|
|
63575
|
+
--x-corner-bottom-left-radius: initial;
|
|
63576
|
+
|
|
63045
63577
|
display: contents;
|
|
63046
63578
|
text-align: initial; /* Don't inherit picker text align */
|
|
63047
63579
|
}
|
|
@@ -63059,6 +63591,12 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
|
|
|
63059
63591
|
--x-picker-icon-color: var(--picker-icon-color-readonly);
|
|
63060
63592
|
--x-picker-cursor: default;
|
|
63061
63593
|
}
|
|
63594
|
+
/* Read-only and still opening, so it still says so under the pointer.
|
|
63595
|
+
Before the disabled block below on purpose: a disabled picker opens
|
|
63596
|
+
nothing, read-only or not. */
|
|
63597
|
+
&[data-readonly-opens] {
|
|
63598
|
+
--x-picker-cursor: pointer;
|
|
63599
|
+
}
|
|
63062
63600
|
/* Focus */
|
|
63063
63601
|
&[data-focus-within]:has(.navi_picker_input[data-focus-visible]) {
|
|
63064
63602
|
--x-picker-border-color: transparent;
|
|
@@ -63187,6 +63725,7 @@ const PickerButton = props => {
|
|
|
63187
63725
|
// untouched (see the --navi-clear command).
|
|
63188
63726
|
clearConfirm,
|
|
63189
63727
|
clearConfirmPopupContent,
|
|
63728
|
+
readOnly,
|
|
63190
63729
|
error
|
|
63191
63730
|
} = props;
|
|
63192
63731
|
const isSingleLine = maxLines === 1;
|
|
@@ -63204,6 +63743,19 @@ const PickerButton = props => {
|
|
|
63204
63743
|
children
|
|
63205
63744
|
} = inputProps;
|
|
63206
63745
|
const loading = basePseudoState[":-navi-loading"];
|
|
63746
|
+
// The same chain useControlProps resolves (own prop first, then what the
|
|
63747
|
+
// group above says), for the two things it does not carry: the popup content,
|
|
63748
|
+
// which is read-only along with the picker, and the cursor, which stays a
|
|
63749
|
+
// pointer on a picker that still opens.
|
|
63750
|
+
const readOnlyFromAbove = useContext(ReadOnlyContext);
|
|
63751
|
+
const readOnlyResolved = readOnly || readOnlyFromAbove;
|
|
63752
|
+
// Read off the controller rather than worked out again: what makes a
|
|
63753
|
+
// read-only picker open is settled once, where the gate reads it (see
|
|
63754
|
+
// createControlInfo's readOnlyOpens). Needed here for the cursor.
|
|
63755
|
+
const readOnlyOpens = Boolean(readOnlyResolved) && uiStateController.readOnlyOpens;
|
|
63756
|
+
// Whether anything can still be changed — read by the clear cross below,
|
|
63757
|
+
// clearing being a modification like any other.
|
|
63758
|
+
const interactive = !basePseudoState[":disabled"] && !basePseudoState[":read-only"] && !loading;
|
|
63207
63759
|
usePickerErrorCallout(uiStateController, error);
|
|
63208
63760
|
return jsxs(Box, {
|
|
63209
63761
|
as: "div",
|
|
@@ -63222,6 +63774,7 @@ const PickerButton = props => {
|
|
|
63222
63774
|
"navi-picker": "",
|
|
63223
63775
|
"navi-single-line": isSingleLine ? "" : undefined,
|
|
63224
63776
|
"navi-ui-custom": ui === "default" ? undefined : "",
|
|
63777
|
+
"data-readonly-opens": readOnlyOpens ? "" : undefined,
|
|
63225
63778
|
"data-popup-width-fit-content": popupWidthFitContent ? "" : undefined,
|
|
63226
63779
|
...pickerRemainingProps,
|
|
63227
63780
|
basePseudoState: basePseudoState,
|
|
@@ -63232,6 +63785,7 @@ const PickerButton = props => {
|
|
|
63232
63785
|
rightSlot: undefined,
|
|
63233
63786
|
clearConfirm: undefined,
|
|
63234
63787
|
clearConfirmPopupContent: undefined,
|
|
63788
|
+
openWhileReadOnly: undefined,
|
|
63235
63789
|
ui: undefined,
|
|
63236
63790
|
maxLines: undefined,
|
|
63237
63791
|
popupWidthFitContent: undefined,
|
|
@@ -63350,7 +63904,7 @@ const PickerButton = props => {
|
|
|
63350
63904
|
value: undefined,
|
|
63351
63905
|
children: jsx(ControlNameContext.Provider, {
|
|
63352
63906
|
value: undefined,
|
|
63353
|
-
children: clearable && value !== undefined && value !== "" ? jsx(Button, {
|
|
63907
|
+
children: clearable && interactive && value !== undefined && value !== "" ? jsx(Button, {
|
|
63354
63908
|
command: "--navi-clear",
|
|
63355
63909
|
commandFor: inputProps.id
|
|
63356
63910
|
// The question, asked before the clear rather than by the
|
|
@@ -63403,9 +63957,12 @@ const PickerButton = props => {
|
|
|
63403
63957
|
})]
|
|
63404
63958
|
}), jsx(ControlFacadeChildrenWrapper, {
|
|
63405
63959
|
...facadeChildrenProps,
|
|
63406
|
-
children: jsx(
|
|
63407
|
-
|
|
63408
|
-
children:
|
|
63960
|
+
children: jsx(ReadOnlyContext.Provider, {
|
|
63961
|
+
value: readOnlyResolved,
|
|
63962
|
+
children: jsx("div", {
|
|
63963
|
+
className: "navi_picker_content",
|
|
63964
|
+
children: children
|
|
63965
|
+
})
|
|
63409
63966
|
})
|
|
63410
63967
|
})]
|
|
63411
63968
|
});
|
|
@@ -63664,7 +64221,27 @@ const css$t = /* css */`
|
|
|
63664
64221
|
of one's own still wins — an inline style beats a stylesheet. */
|
|
63665
64222
|
border: var(--navi-control-border-width) solid
|
|
63666
64223
|
var(--navi-control-border-color);
|
|
63667
|
-
|
|
64224
|
+
/* Squared from the outside, corner by corner: a spin is not always the
|
|
64225
|
+
member a Group joins — it can arrive wrapped — so the ask travels down
|
|
64226
|
+
as inherited custom properties rather than as a radius landing on this
|
|
64227
|
+
element. The chevrons take these corners back by inherit (see below), so
|
|
64228
|
+
they follow. */
|
|
64229
|
+
border-top-left-radius: var(
|
|
64230
|
+
--x-corner-top-left-radius,
|
|
64231
|
+
var(--navi-control-border-radius)
|
|
64232
|
+
);
|
|
64233
|
+
border-top-right-radius: var(
|
|
64234
|
+
--x-corner-top-right-radius,
|
|
64235
|
+
var(--navi-control-border-radius)
|
|
64236
|
+
);
|
|
64237
|
+
border-bottom-right-radius: var(
|
|
64238
|
+
--x-corner-bottom-right-radius,
|
|
64239
|
+
var(--navi-control-border-radius)
|
|
64240
|
+
);
|
|
64241
|
+
border-bottom-left-radius: var(
|
|
64242
|
+
--x-corner-bottom-left-radius,
|
|
64243
|
+
var(--navi-control-border-radius)
|
|
64244
|
+
);
|
|
63668
64245
|
outline-width: var(--navi-focus-outline-width);
|
|
63669
64246
|
/* Just outside the border, never on it: the ring belongs to the whole
|
|
63670
64247
|
control — the two chevrons included, since pressing one lands the
|
|
@@ -63721,6 +64298,13 @@ const css$t = /* css */`
|
|
|
63721
64298
|
opens its calendar. Around the whole control it would open under a chevron;
|
|
63722
64299
|
around the value it opens under the value one pressed. */
|
|
63723
64300
|
.navi_picker_spin_middle {
|
|
64301
|
+
/* The ask stops here: the frame is the spin's box, and the picker or the
|
|
64302
|
+
field standing in the middle of it is behind that frame, not at a seam. */
|
|
64303
|
+
--x-corner-top-left-radius: initial;
|
|
64304
|
+
--x-corner-top-right-radius: initial;
|
|
64305
|
+
--x-corner-bottom-right-radius: initial;
|
|
64306
|
+
--x-corner-bottom-left-radius: initial;
|
|
64307
|
+
|
|
63724
64308
|
position: relative;
|
|
63725
64309
|
display: flex;
|
|
63726
64310
|
min-width: 0;
|
|
@@ -63898,7 +64482,26 @@ const css$t = /* css */`
|
|
|
63898
64482
|
font-family: var(--navi-control-font-family);
|
|
63899
64483
|
border: var(--navi-control-border-width) solid
|
|
63900
64484
|
var(--navi-control-border-color);
|
|
63901
|
-
|
|
64485
|
+
/* Squared from the outside, corner by corner (see .navi_picker_spin
|
|
64486
|
+
above): the group is the member a Group joins, and it can arrive
|
|
64487
|
+
wrapped. The spins inside give their radius up and take the corners of
|
|
64488
|
+
this one back through inherit, so they follow. */
|
|
64489
|
+
border-top-left-radius: var(
|
|
64490
|
+
--x-corner-top-left-radius,
|
|
64491
|
+
var(--navi-control-border-radius)
|
|
64492
|
+
);
|
|
64493
|
+
border-top-right-radius: var(
|
|
64494
|
+
--x-corner-top-right-radius,
|
|
64495
|
+
var(--navi-control-border-radius)
|
|
64496
|
+
);
|
|
64497
|
+
border-bottom-right-radius: var(
|
|
64498
|
+
--x-corner-bottom-right-radius,
|
|
64499
|
+
var(--navi-control-border-radius)
|
|
64500
|
+
);
|
|
64501
|
+
border-bottom-left-radius: var(
|
|
64502
|
+
--x-corner-bottom-left-radius,
|
|
64503
|
+
var(--navi-control-border-radius)
|
|
64504
|
+
);
|
|
63902
64505
|
outline-width: var(--navi-focus-outline-width);
|
|
63903
64506
|
outline-color: var(--navi-focus-outline-color);
|
|
63904
64507
|
outline-offset: 0px;
|