@jsenv/navi 0.29.481 → 0.29.483
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev/jsenv_navi.js +726 -105
- package/dist/dev/jsenv_navi.js.map +34 -30
- package/dist/jsenv_navi.js +523 -109
- package/dist/jsenv_navi.js.map +32 -30
- package/docs/data_states.md +6 -0
- package/docs/dynamic_import.md +4 -2
- package/docs/navigation.md +9 -1
- package/docs/popup_open.md +9 -1
- package/docs/testid.md +6 -2
- package/package.json +3 -3
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -980,6 +980,14 @@ const buttonI18n = /*#__PURE__*/ defineNaviMessages({
|
|
|
980
980
|
en: "Remove",
|
|
981
981
|
fr: "Retirer",
|
|
982
982
|
},
|
|
983
|
+
"button.copy": {
|
|
984
|
+
en: "Copy",
|
|
985
|
+
fr: "Copier",
|
|
986
|
+
},
|
|
987
|
+
"button.copied": {
|
|
988
|
+
en: "Copied!",
|
|
989
|
+
fr: "Copié !",
|
|
990
|
+
},
|
|
983
991
|
});
|
|
984
992
|
|
|
985
993
|
// Spin messages — the ends of what one steps through when the control holding
|
|
@@ -2964,6 +2972,78 @@ const mergeTwoJsValues = (firstValue, secondValue) => {
|
|
|
2964
2972
|
return objectMerge;
|
|
2965
2973
|
};
|
|
2966
2974
|
|
|
2975
|
+
// An option a function does not read does nothing, and what is built on it
|
|
2976
|
+
// still looks plausible: `persists` written `persist` keeps nothing across a
|
|
2977
|
+
// reload, and nothing says so. Said out loud in dev, with the option it most
|
|
2978
|
+
// likely meant, because the caller cannot tell "ignored" from "broken".
|
|
2979
|
+
|
|
2980
|
+
/**
|
|
2981
|
+
* Warns, once per `where` and key, about each key of `options` that is not
|
|
2982
|
+
* in `knownKeys`. `hints` maps a key to a sentence saying where that wish
|
|
2983
|
+
* belongs instead.
|
|
2984
|
+
*/
|
|
2985
|
+
const warnUnknownOptions = (where, options, knownKeys, hints = {}) => {
|
|
2986
|
+
for (const key of Object.keys(options)) {
|
|
2987
|
+
if (knownKeys.has(key)) {
|
|
2988
|
+
continue;
|
|
2989
|
+
}
|
|
2990
|
+
const warnedKey = `${where} ${key}`;
|
|
2991
|
+
if (warnedKeySet.has(warnedKey)) {
|
|
2992
|
+
continue;
|
|
2993
|
+
}
|
|
2994
|
+
warnedKeySet.add(warnedKey);
|
|
2995
|
+
let message = `${where}: "${key}" is not an option it reads, it does nothing.`;
|
|
2996
|
+
const closest = findClosestKey(key, knownKeys);
|
|
2997
|
+
if (closest) {
|
|
2998
|
+
message += ` Did you mean "${closest}"?`;
|
|
2999
|
+
}
|
|
3000
|
+
if (hints[key]) {
|
|
3001
|
+
message += ` ${hints[key]}`;
|
|
3002
|
+
}
|
|
3003
|
+
console.warn(message);
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
|
|
3007
|
+
const warnedKeySet = new Set();
|
|
3008
|
+
|
|
3009
|
+
// A slip of a letter or two (`persist`, `debounceURL`), or the same word
|
|
3010
|
+
// with a different case.
|
|
3011
|
+
const findClosestKey = (key, knownKeys) => {
|
|
3012
|
+
const keyLowerCase = key.toLowerCase();
|
|
3013
|
+
let closest;
|
|
3014
|
+
let closestDistance = 3;
|
|
3015
|
+
for (const knownKey of knownKeys) {
|
|
3016
|
+
const distance = editDistance(keyLowerCase, knownKey.toLowerCase());
|
|
3017
|
+
if (distance < closestDistance) {
|
|
3018
|
+
closest = knownKey;
|
|
3019
|
+
closestDistance = distance;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
return closest;
|
|
3023
|
+
};
|
|
3024
|
+
|
|
3025
|
+
const editDistance = (a, b) => {
|
|
3026
|
+
let previousRow = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
3027
|
+
for (let i = 1; i <= a.length; i++) {
|
|
3028
|
+
const row = [i];
|
|
3029
|
+
for (let j = 1; j <= b.length; j++) {
|
|
3030
|
+
const substitution = previousRow[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
3031
|
+
const insertion = row[j - 1] + 1;
|
|
3032
|
+
const deletion = previousRow[j] + 1;
|
|
3033
|
+
let smallest = substitution;
|
|
3034
|
+
if (insertion < smallest) {
|
|
3035
|
+
smallest = insertion;
|
|
3036
|
+
}
|
|
3037
|
+
if (deletion < smallest) {
|
|
3038
|
+
smallest = deletion;
|
|
3039
|
+
}
|
|
3040
|
+
row.push(smallest);
|
|
3041
|
+
}
|
|
3042
|
+
previousRow = row;
|
|
3043
|
+
}
|
|
3044
|
+
return previousRow[b.length];
|
|
3045
|
+
};
|
|
3046
|
+
|
|
2967
3047
|
/**
|
|
2968
3048
|
* Creates an effect that uses WeakRef to prevent garbage collection of referenced values.
|
|
2969
3049
|
*
|
|
@@ -3759,6 +3839,33 @@ const mergeActionParams = (currentParams, newParams) => {
|
|
|
3759
3839
|
return mergeTwoJsValues(currentParams, newParams);
|
|
3760
3840
|
};
|
|
3761
3841
|
|
|
3842
|
+
// What createActionCore reads off the options (see warnUnknownOptions):
|
|
3843
|
+
// `{ dependencies: [X] }` would otherwise be accepted and never rerun anything.
|
|
3844
|
+
const OPTIONS_CREATE_ACTION_READS = new Set([
|
|
3845
|
+
"keep",
|
|
3846
|
+
"name",
|
|
3847
|
+
"params",
|
|
3848
|
+
"isPrerun",
|
|
3849
|
+
"runningState",
|
|
3850
|
+
"aborted",
|
|
3851
|
+
"error",
|
|
3852
|
+
"value",
|
|
3853
|
+
"resultToValue",
|
|
3854
|
+
"valueToData",
|
|
3855
|
+
"provisionalValue",
|
|
3856
|
+
"inheritData",
|
|
3857
|
+
"dataDefault",
|
|
3858
|
+
"data",
|
|
3859
|
+
"completed",
|
|
3860
|
+
"sideEffect",
|
|
3861
|
+
"meta",
|
|
3862
|
+
"outputSignal",
|
|
3863
|
+
"completeSideEffect",
|
|
3864
|
+
]);
|
|
3865
|
+
const OPTIONS_CREATE_ACTION_HINTS = {
|
|
3866
|
+
dependencies: `A read that follows writes is a resource (docs/actions.md, "Reruns").`,
|
|
3867
|
+
rerunOn: `A read that follows writes is a resource (docs/actions.md, "Reruns").`,
|
|
3868
|
+
};
|
|
3762
3869
|
const actionWeakMap = new WeakMap();
|
|
3763
3870
|
/**
|
|
3764
3871
|
* Wraps an async callback into an action: a question, its answer, and the
|
|
@@ -3801,6 +3908,14 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
3801
3908
|
if (existing) {
|
|
3802
3909
|
return existing;
|
|
3803
3910
|
}
|
|
3911
|
+
{
|
|
3912
|
+
warnUnknownOptions(
|
|
3913
|
+
`createAction(${callback.name || "anonymous"})`,
|
|
3914
|
+
rootOptions,
|
|
3915
|
+
OPTIONS_CREATE_ACTION_READS,
|
|
3916
|
+
OPTIONS_CREATE_ACTION_HINTS,
|
|
3917
|
+
);
|
|
3918
|
+
}
|
|
3804
3919
|
|
|
3805
3920
|
const keptAnswerRegistry = rootOptions.keep
|
|
3806
3921
|
? createKeptAnswerRegistry(rootOptions.keep)
|
|
@@ -7316,6 +7431,32 @@ const notePageChange = () => {
|
|
|
7316
7431
|
|
|
7317
7432
|
const isRenderingPageChange = () => renderingPageChange;
|
|
7318
7433
|
|
|
7434
|
+
// A browser refusing storage (blocked site data, some private windows) throws
|
|
7435
|
+
// on the first touch of window.localStorage, and a full one throws on setItem.
|
|
7436
|
+
// Either way what was to be kept lives in memory for the session, which is all
|
|
7437
|
+
// a persisted value can fall back to — so every access goes through here.
|
|
7438
|
+
|
|
7439
|
+
const readLocalStorage = (key) => {
|
|
7440
|
+
try {
|
|
7441
|
+
const raw = window.localStorage.getItem(key);
|
|
7442
|
+
return raw === null ? undefined : raw;
|
|
7443
|
+
} catch {
|
|
7444
|
+
return undefined;
|
|
7445
|
+
}
|
|
7446
|
+
};
|
|
7447
|
+
|
|
7448
|
+
const writeLocalStorage = (key, value) => {
|
|
7449
|
+
try {
|
|
7450
|
+
if (value === undefined) {
|
|
7451
|
+
window.localStorage.removeItem(key);
|
|
7452
|
+
} else {
|
|
7453
|
+
window.localStorage.setItem(key, value);
|
|
7454
|
+
}
|
|
7455
|
+
} catch {
|
|
7456
|
+
// Full, or refused: the value lives for the session only.
|
|
7457
|
+
}
|
|
7458
|
+
};
|
|
7459
|
+
|
|
7319
7460
|
// Global signal registry for route template detection
|
|
7320
7461
|
const globalSignalRegistry = new Map();
|
|
7321
7462
|
|
|
@@ -7351,6 +7492,26 @@ const generateSignalId = () => {
|
|
|
7351
7492
|
return id;
|
|
7352
7493
|
};
|
|
7353
7494
|
|
|
7495
|
+
// Read by stateSignal, or off the options it stores by route_pattern.js
|
|
7496
|
+
// (itemType).
|
|
7497
|
+
const STATE_SIGNAL_OPTIONS = new Set([
|
|
7498
|
+
"id",
|
|
7499
|
+
"type",
|
|
7500
|
+
"itemType",
|
|
7501
|
+
"min",
|
|
7502
|
+
"max",
|
|
7503
|
+
"step",
|
|
7504
|
+
"oneOf",
|
|
7505
|
+
"localStorageRepresentation",
|
|
7506
|
+
"persists",
|
|
7507
|
+
"debug",
|
|
7508
|
+
"default",
|
|
7509
|
+
"ignoreArrayOrder",
|
|
7510
|
+
"autoFix",
|
|
7511
|
+
"weak",
|
|
7512
|
+
"history",
|
|
7513
|
+
"debounceUrl",
|
|
7514
|
+
]);
|
|
7354
7515
|
/**
|
|
7355
7516
|
* Creates an advanced signal with dynamic default value, local storage persistence, and validation.
|
|
7356
7517
|
*
|
|
@@ -7493,6 +7654,9 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
7493
7654
|
history = "replace",
|
|
7494
7655
|
debounceUrl = 0,
|
|
7495
7656
|
} = options;
|
|
7657
|
+
{
|
|
7658
|
+
warnUnknownOptions(`stateSignal "${id}"`, options, STATE_SIGNAL_OPTIONS);
|
|
7659
|
+
}
|
|
7496
7660
|
|
|
7497
7661
|
if (history !== "replace" && history !== "push") {
|
|
7498
7662
|
throw new TypeError(
|
|
@@ -7561,27 +7725,19 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
7561
7725
|
}
|
|
7562
7726
|
}
|
|
7563
7727
|
const readFromLocalStorage = persists
|
|
7564
|
-
? () =>
|
|
7565
|
-
const raw = window.localStorage.getItem(localStorageKey);
|
|
7566
|
-
if (raw === null) {
|
|
7567
|
-
return undefined;
|
|
7568
|
-
}
|
|
7569
|
-
return raw;
|
|
7570
|
-
}
|
|
7728
|
+
? () => readLocalStorage(localStorageKey)
|
|
7571
7729
|
: () => undefined;
|
|
7572
7730
|
const updateLocalStorage = persists
|
|
7573
7731
|
? () => {
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
window.localStorage.setItem(localStorageKey, localStorageValue);
|
|
7579
|
-
}
|
|
7732
|
+
writeLocalStorage(
|
|
7733
|
+
localStorageKey,
|
|
7734
|
+
validity.representations.localStorage.value,
|
|
7735
|
+
);
|
|
7580
7736
|
}
|
|
7581
7737
|
: () => {};
|
|
7582
7738
|
const removeFromLocalStorage = persists
|
|
7583
7739
|
? () => {
|
|
7584
|
-
|
|
7740
|
+
writeLocalStorage(localStorageKey, undefined);
|
|
7585
7741
|
}
|
|
7586
7742
|
: () => {};
|
|
7587
7743
|
|
|
@@ -10289,6 +10445,13 @@ const getRoutePrivateProperties = (route) => {
|
|
|
10289
10445
|
const ROUTE_NOT_MATCHING_PARAMS = {};
|
|
10290
10446
|
// Flag to prevent signal-to-URL synchronization during URL-to-signal synchronization
|
|
10291
10447
|
let isUpdatingRoutesFromUrl = false;
|
|
10448
|
+
const ROUTE_OPTIONS = new Set([
|
|
10449
|
+
"searchParams",
|
|
10450
|
+
"params",
|
|
10451
|
+
"redirectRoute",
|
|
10452
|
+
"redirectRouteParams",
|
|
10453
|
+
"dropSearchParams",
|
|
10454
|
+
]);
|
|
10292
10455
|
/**
|
|
10293
10456
|
* Declares a route from an url pattern.
|
|
10294
10457
|
*
|
|
@@ -10371,16 +10534,17 @@ let isUpdatingRoutesFromUrl = false;
|
|
|
10371
10534
|
* route(`/games/:gameId=${gamePageIdSignal}`, { dropSearchParams: ["v"] });
|
|
10372
10535
|
* ```
|
|
10373
10536
|
*/
|
|
10374
|
-
const route = (
|
|
10375
|
-
|
|
10376
|
-
{
|
|
10537
|
+
const route = (pattern, options = {}) => {
|
|
10538
|
+
const {
|
|
10377
10539
|
searchParams,
|
|
10378
10540
|
params,
|
|
10379
10541
|
redirectRoute,
|
|
10380
10542
|
redirectRouteParams,
|
|
10381
10543
|
dropSearchParams,
|
|
10382
|
-
} =
|
|
10383
|
-
|
|
10544
|
+
} = options;
|
|
10545
|
+
{
|
|
10546
|
+
warnUnknownOptions(`route("${pattern}")`, options, ROUTE_OPTIONS);
|
|
10547
|
+
}
|
|
10384
10548
|
const routePattern = createRoutePattern(pattern, { searchParams, params });
|
|
10385
10549
|
const { cleanPattern } = routePattern;
|
|
10386
10550
|
const [publishStatus, subscribeStatus] = createPubSub();
|
|
@@ -12086,6 +12250,13 @@ const activeRouteReadsSignal = computed(() => {
|
|
|
12086
12250
|
return reads;
|
|
12087
12251
|
});
|
|
12088
12252
|
|
|
12253
|
+
// Besides `when` and `prefetch`, what the binding reads (actionRunEffect,
|
|
12254
|
+
// then bindParams).
|
|
12255
|
+
const ROUTE_ACTION_BINDING_OPTIONS = new Set([
|
|
12256
|
+
"debounce",
|
|
12257
|
+
"inheritData",
|
|
12258
|
+
"rerunOnChange",
|
|
12259
|
+
]);
|
|
12089
12260
|
/**
|
|
12090
12261
|
* Binds an action to a route: it runs when the route matches, with the params
|
|
12091
12262
|
* the effect reads off the address, runs again on a `reload()`, and is aborted
|
|
@@ -12135,6 +12306,9 @@ const routeAction = (
|
|
|
12135
12306
|
paramsEffect,
|
|
12136
12307
|
{ when, prefetch = true, ...options } = {},
|
|
12137
12308
|
) => {
|
|
12309
|
+
{
|
|
12310
|
+
warnUnknownOptions("routeAction", options, ROUTE_ACTION_BINDING_OPTIONS);
|
|
12311
|
+
}
|
|
12138
12312
|
if (typeof action === "function") {
|
|
12139
12313
|
action = createAction(action);
|
|
12140
12314
|
}
|
|
@@ -13977,6 +14151,12 @@ const stopLoad = (reason = "stopLoad() called") => {
|
|
|
13977
14151
|
* actions of the routes matching the url run again (see routeAction), one that
|
|
13978
14152
|
* failed included — what a "Retry" under a page that could not load calls.
|
|
13979
14153
|
* The address, the history and the scroll stay as they are.
|
|
14154
|
+
*
|
|
14155
|
+
* Route actions only: a component's `useAsyncData(fn)`, or an action no route
|
|
14156
|
+
* reads, is not asked again. Nor does it bring back code whose `import()`
|
|
14157
|
+
* failed — the document refuses that address again without a request — so
|
|
14158
|
+
* the way out of a failed import is a fresh document,
|
|
14159
|
+
* `window.location.reload()` (docs/dynamic_import.md).
|
|
13980
14160
|
*/
|
|
13981
14161
|
const reload = browserIntegration.reload;
|
|
13982
14162
|
/**
|
|
@@ -25212,16 +25392,11 @@ const arraySignalMembership = (...args) => {
|
|
|
25212
25392
|
};
|
|
25213
25393
|
|
|
25214
25394
|
const localStorageSignal = (key) => {
|
|
25215
|
-
const
|
|
25216
|
-
|
|
25217
|
-
const valueSignal = signal(initialValue === null ? undefined : initialValue);
|
|
25395
|
+
const valueSignal = signal(readLocalStorage(key));
|
|
25218
25396
|
effect(() => {
|
|
25219
25397
|
const value = valueSignal.value;
|
|
25220
|
-
|
|
25221
|
-
|
|
25222
|
-
} else {
|
|
25223
|
-
localStorage.setItem(key, value);
|
|
25224
|
-
}
|
|
25398
|
+
// Storage keeps strings: a null written as is would come back as "null".
|
|
25399
|
+
writeLocalStorage(key, value === null ? undefined : value);
|
|
25225
25400
|
});
|
|
25226
25401
|
|
|
25227
25402
|
return valueSignal;
|
|
@@ -26067,12 +26242,51 @@ const resolveRerunOn = (rerunOn, inheritedRerunOn = defaultRerunOn) => {
|
|
|
26067
26242
|
if (!rerunOn) {
|
|
26068
26243
|
return inheritedRerunOn;
|
|
26069
26244
|
}
|
|
26245
|
+
assertRerunOnCanFire(rerunOn);
|
|
26070
26246
|
return {
|
|
26071
26247
|
GET: rerunOn.GET ?? inheritedRerunOn.GET,
|
|
26072
26248
|
GET_MANY: rerunOn.GET_MANY ?? inheritedRerunOn.GET_MANY,
|
|
26073
26249
|
GET_RANGE: rerunOn.GET_RANGE ?? inheritedRerunOn.GET_RANGE,
|
|
26074
26250
|
};
|
|
26075
26251
|
};
|
|
26252
|
+
// What can never fire throws, as a dependency name does (see
|
|
26253
|
+
// resolveDependency): a rerun that never happens leaves a read on its old
|
|
26254
|
+
// value with nothing to say so.
|
|
26255
|
+
const RERUN_READS = ["GET", "GET_MANY", "GET_RANGE"];
|
|
26256
|
+
const RERUN_VERBS = ["POST", "PUT", "PATCH", "DELETE", "*"];
|
|
26257
|
+
const assertRerunOnCanFire = (rerunOn) => {
|
|
26258
|
+
for (const read of Object.keys(rerunOn)) {
|
|
26259
|
+
if (!RERUN_READS.includes(read)) {
|
|
26260
|
+
throw new Error(
|
|
26261
|
+
`rerunOn: "${read}" is not a read that reruns — rerunOn takes GET, GET_MANY and GET_RANGE`,
|
|
26262
|
+
);
|
|
26263
|
+
}
|
|
26264
|
+
const verbs = rerunOn[read];
|
|
26265
|
+
if (
|
|
26266
|
+
verbs === undefined ||
|
|
26267
|
+
verbs === null ||
|
|
26268
|
+
verbs === false ||
|
|
26269
|
+
verbs === "*"
|
|
26270
|
+
) {
|
|
26271
|
+
continue;
|
|
26272
|
+
}
|
|
26273
|
+
if (!Array.isArray(verbs)) {
|
|
26274
|
+
throw new Error(
|
|
26275
|
+
`rerunOn.${read}: ${JSON.stringify(verbs)} never fires — it takes false, "*" or a list of verbs`,
|
|
26276
|
+
);
|
|
26277
|
+
}
|
|
26278
|
+
for (const verb of verbs) {
|
|
26279
|
+
if (
|
|
26280
|
+
typeof verb !== "string" ||
|
|
26281
|
+
!RERUN_VERBS.includes(verb.toUpperCase())
|
|
26282
|
+
) {
|
|
26283
|
+
throw new Error(
|
|
26284
|
+
`rerunOn.${read}: ${JSON.stringify(verb)} never fires — rerunOn names verbs (POST, PUT, PATCH, DELETE or "*"): POST covers POST_MANY`,
|
|
26285
|
+
);
|
|
26286
|
+
}
|
|
26287
|
+
}
|
|
26288
|
+
}
|
|
26289
|
+
};
|
|
26076
26290
|
|
|
26077
26291
|
// What a `dependencies` entry names are callbacks, not verbs (`rerunOn` names
|
|
26078
26292
|
// verbs, where `PATCH` covers `PATCH_MANY`): one callback of a resource can be
|
|
@@ -27101,6 +27315,24 @@ const debug$2 = (...args) => {
|
|
|
27101
27315
|
}
|
|
27102
27316
|
};
|
|
27103
27317
|
|
|
27318
|
+
const RESOURCE_OPTIONS = new Set([
|
|
27319
|
+
"idKey",
|
|
27320
|
+
"uniqueKeys",
|
|
27321
|
+
"rerunOn",
|
|
27322
|
+
"dependencies",
|
|
27323
|
+
"persist",
|
|
27324
|
+
"GET",
|
|
27325
|
+
"GET_MANY",
|
|
27326
|
+
"GET_RANGE",
|
|
27327
|
+
"POST",
|
|
27328
|
+
"POST_MANY",
|
|
27329
|
+
"PUT",
|
|
27330
|
+
"PUT_MANY",
|
|
27331
|
+
"PATCH",
|
|
27332
|
+
"PATCH_MANY",
|
|
27333
|
+
"DELETE",
|
|
27334
|
+
"DELETE_MANY",
|
|
27335
|
+
]);
|
|
27104
27336
|
/**
|
|
27105
27337
|
* Creates a reactive REST resource backed by a shared signal store.
|
|
27106
27338
|
* Returns a `stateFacade` exposing one action per REST callback provided
|
|
@@ -27159,9 +27391,8 @@ const debug$2 = (...args) => {
|
|
|
27159
27391
|
* DELETE: ({ id }) => fetchJson(`/users/${id}`, { method: "DELETE" }),
|
|
27160
27392
|
* });
|
|
27161
27393
|
*/
|
|
27162
|
-
const resource = (
|
|
27163
|
-
|
|
27164
|
-
{
|
|
27394
|
+
const resource = (name, restCallbacks = {}) => {
|
|
27395
|
+
const {
|
|
27165
27396
|
// configuration options
|
|
27166
27397
|
idKey = "id",
|
|
27167
27398
|
uniqueKeys = [],
|
|
@@ -27180,8 +27411,10 @@ const resource = (
|
|
|
27180
27411
|
PATCH_MANY,
|
|
27181
27412
|
DELETE,
|
|
27182
27413
|
DELETE_MANY,
|
|
27183
|
-
} =
|
|
27184
|
-
|
|
27414
|
+
} = restCallbacks;
|
|
27415
|
+
{
|
|
27416
|
+
warnUnknownOptions(`resource(${name})`, restCallbacks, RESOURCE_OPTIONS);
|
|
27417
|
+
}
|
|
27185
27418
|
const declarationSite = getDeclarationSite();
|
|
27186
27419
|
const setupCallbackSet = new Set();
|
|
27187
27420
|
const addItemSetup = (callback) => {
|
|
@@ -29073,8 +29306,8 @@ const valueInLocalStorage = (key, { type = "any" } = {}) => {
|
|
|
29073
29306
|
const converter = TYPE_CONVERTERS[type];
|
|
29074
29307
|
|
|
29075
29308
|
const get = () => {
|
|
29076
|
-
|
|
29077
|
-
if (valueInLocalStorage ===
|
|
29309
|
+
const valueInLocalStorage = readLocalStorage(key);
|
|
29310
|
+
if (valueInLocalStorage === undefined) {
|
|
29078
29311
|
return undefined;
|
|
29079
29312
|
}
|
|
29080
29313
|
let valueToReturn = valueInLocalStorage;
|
|
@@ -29098,7 +29331,7 @@ const valueInLocalStorage = (key, { type = "any" } = {}) => {
|
|
|
29098
29331
|
|
|
29099
29332
|
const set = (value) => {
|
|
29100
29333
|
if (value === undefined) {
|
|
29101
|
-
|
|
29334
|
+
writeLocalStorage(key, undefined);
|
|
29102
29335
|
return;
|
|
29103
29336
|
}
|
|
29104
29337
|
let valueToStore = value;
|
|
@@ -29106,10 +29339,10 @@ const valueInLocalStorage = (key, { type = "any" } = {}) => {
|
|
|
29106
29339
|
const valueEncoded = converter.encode(valueToStore);
|
|
29107
29340
|
valueToStore = valueEncoded;
|
|
29108
29341
|
}
|
|
29109
|
-
|
|
29342
|
+
writeLocalStorage(key, valueToStore);
|
|
29110
29343
|
};
|
|
29111
29344
|
const remove = () => {
|
|
29112
|
-
|
|
29345
|
+
writeLocalStorage(key, undefined);
|
|
29113
29346
|
};
|
|
29114
29347
|
|
|
29115
29348
|
return [get, set, remove];
|
|
@@ -29329,7 +29562,8 @@ const useForceRender = () => {
|
|
|
29329
29562
|
*
|
|
29330
29563
|
* @example <caption>Handle error inline</caption>
|
|
29331
29564
|
* const [user, , error] = useAsyncData(userAction, { error: true });
|
|
29332
|
-
*
|
|
29565
|
+
* // A failed refresh keeps `user`: only a first load has nothing else to show.
|
|
29566
|
+
* if (error && user === undefined) return <ErrorCard message={error.message} />;
|
|
29333
29567
|
*
|
|
29334
29568
|
* @example <caption>Say the failure over the rows already on screen</caption>
|
|
29335
29569
|
* const [items, , error, dismissError] = useAsyncData(listAction, { error: true });
|
|
@@ -41288,9 +41522,12 @@ const confirmCommand = (source, event) => {
|
|
|
41288
41522
|
event,
|
|
41289
41523
|
source: commandSource,
|
|
41290
41524
|
});
|
|
41525
|
+
// Carried by the close too: a Dialog or a Popover answers the yes in
|
|
41526
|
+
// its onClose (detail.isConfirm), the twin of isCancel.
|
|
41291
41527
|
return dispatchCustomEvent(target, "navi_request_close", {
|
|
41292
41528
|
event,
|
|
41293
41529
|
source: commandSource,
|
|
41530
|
+
isConfirm: true,
|
|
41294
41531
|
});
|
|
41295
41532
|
},
|
|
41296
41533
|
};
|
|
@@ -43058,6 +43295,14 @@ const useParallelGuard = (max) => {
|
|
|
43058
43295
|
* `getAssociatedLabels` in control_hooks.jsx. Both channels carry the same
|
|
43059
43296
|
* values and land on the same setters, so a control reachable through both is
|
|
43060
43297
|
* simply told twice.
|
|
43298
|
+
*
|
|
43299
|
+
* The link also runs the other way, for a control whose role is on its root
|
|
43300
|
+
* (`nameOn: "root"`, a wheel): a label[for] can only point at its host, an
|
|
43301
|
+
* aria-hidden carrier, and names nothing. That control says so in the state it
|
|
43302
|
+
* publishes; its label then drops `for` and publishes its own id under the
|
|
43303
|
+
* control's id instead, and the control names its root with the ids it finds
|
|
43304
|
+
* there (aria-labelledby). A label[for] kept as well would name the carrier
|
|
43305
|
+
* too, and a lookup by label would find two elements.
|
|
43061
43306
|
*/
|
|
43062
43307
|
|
|
43063
43308
|
const stateByControlId = new Map();
|
|
@@ -43110,6 +43355,68 @@ const subscribeToControlState = (controlId, callback) => {
|
|
|
43110
43355
|
};
|
|
43111
43356
|
};
|
|
43112
43357
|
|
|
43358
|
+
const labelIdSetByControlId = new Map();
|
|
43359
|
+
const labelCallbackSetByControlId = new Map();
|
|
43360
|
+
|
|
43361
|
+
const notifyLabelIds = (controlId) => {
|
|
43362
|
+
const callbackSet = labelCallbackSetByControlId.get(controlId);
|
|
43363
|
+
if (!callbackSet) {
|
|
43364
|
+
return;
|
|
43365
|
+
}
|
|
43366
|
+
const labelIds = readLabelIds(controlId);
|
|
43367
|
+
for (const callback of callbackSet) {
|
|
43368
|
+
callback(labelIds);
|
|
43369
|
+
}
|
|
43370
|
+
};
|
|
43371
|
+
// One string ("label_a label_b"), the shape aria-labelledby takes, and equal
|
|
43372
|
+
// from one call to the next while nothing changed.
|
|
43373
|
+
const readLabelIds = (controlId) => {
|
|
43374
|
+
const labelIdSet = labelIdSetByControlId.get(controlId);
|
|
43375
|
+
return labelIdSet ? Array.from(labelIdSet).join(" ") : undefined;
|
|
43376
|
+
};
|
|
43377
|
+
|
|
43378
|
+
/**
|
|
43379
|
+
* Publishes `labelId` as a label of the control identified by `controlId`.
|
|
43380
|
+
* Returns the teardown.
|
|
43381
|
+
*/
|
|
43382
|
+
const publishLabelToControl = (controlId, labelId) => {
|
|
43383
|
+
let labelIdSet = labelIdSetByControlId.get(controlId);
|
|
43384
|
+
if (!labelIdSet) {
|
|
43385
|
+
labelIdSet = new Set();
|
|
43386
|
+
labelIdSetByControlId.set(controlId, labelIdSet);
|
|
43387
|
+
}
|
|
43388
|
+
labelIdSet.add(labelId);
|
|
43389
|
+
notifyLabelIds(controlId);
|
|
43390
|
+
return () => {
|
|
43391
|
+
labelIdSet.delete(labelId);
|
|
43392
|
+
if (labelIdSet.size === 0) {
|
|
43393
|
+
labelIdSetByControlId.delete(controlId);
|
|
43394
|
+
}
|
|
43395
|
+
notifyLabelIds(controlId);
|
|
43396
|
+
};
|
|
43397
|
+
};
|
|
43398
|
+
|
|
43399
|
+
/**
|
|
43400
|
+
* Subscribes to the ids of the labels published for `controlId`. The callback
|
|
43401
|
+
* is called right away (with `undefined` when there is none), then on every
|
|
43402
|
+
* change. Returns the teardown.
|
|
43403
|
+
*/
|
|
43404
|
+
const subscribeToControlLabels = (controlId, callback) => {
|
|
43405
|
+
let callbackSet = labelCallbackSetByControlId.get(controlId);
|
|
43406
|
+
if (!callbackSet) {
|
|
43407
|
+
callbackSet = new Set();
|
|
43408
|
+
labelCallbackSetByControlId.set(controlId, callbackSet);
|
|
43409
|
+
}
|
|
43410
|
+
callbackSet.add(callback);
|
|
43411
|
+
callback(readLabelIds(controlId));
|
|
43412
|
+
return () => {
|
|
43413
|
+
callbackSet.delete(callback);
|
|
43414
|
+
if (callbackSet.size === 0) {
|
|
43415
|
+
labelCallbackSetByControlId.delete(controlId);
|
|
43416
|
+
}
|
|
43417
|
+
};
|
|
43418
|
+
};
|
|
43419
|
+
|
|
43113
43420
|
const FormContext = createContext();
|
|
43114
43421
|
|
|
43115
43422
|
/**
|
|
@@ -47522,17 +47829,11 @@ const useControlProps = (props, {
|
|
|
47522
47829
|
};
|
|
47523
47830
|
}
|
|
47524
47831
|
if (defaultAction === "activate") {
|
|
47525
|
-
// activating the control (e.g. space on a button/range)
|
|
47526
47832
|
return {
|
|
47527
47833
|
name: `keydown to activate`,
|
|
47528
47834
|
prevented: () => e.preventDefault(),
|
|
47529
47835
|
allowed: () => {
|
|
47530
47836
|
triggerUIAction(e);
|
|
47531
|
-
if (controlType === "button" && e.key === " ") {
|
|
47532
|
-
// prevent browser dispatching click
|
|
47533
|
-
// it could lead to duplicates as we explicitely handled the space to click here
|
|
47534
|
-
e.preventDefault();
|
|
47535
|
-
}
|
|
47536
47837
|
}
|
|
47537
47838
|
};
|
|
47538
47839
|
}
|
|
@@ -47669,7 +47970,15 @@ const useControlProps = (props, {
|
|
|
47669
47970
|
runWhenActionSucceeded(completion, deferredCommand);
|
|
47670
47971
|
};
|
|
47671
47972
|
return {
|
|
47672
|
-
keyDown:
|
|
47973
|
+
keyDown: e => {
|
|
47974
|
+
if (getKeyboardEventDefaultAction(e) === "activate") {
|
|
47975
|
+
// The browser presses the button itself — Enter clicks on
|
|
47976
|
+
// keydown, Space on keyup — and the click reaction below answers
|
|
47977
|
+
// that press, once: ui action, action and command together.
|
|
47978
|
+
return null;
|
|
47979
|
+
}
|
|
47980
|
+
return keyDownDefault(e);
|
|
47981
|
+
},
|
|
47673
47982
|
mouseDown: e => {
|
|
47674
47983
|
if (actsOnMouseDown) {
|
|
47675
47984
|
return {
|
|
@@ -47688,7 +47997,9 @@ const useControlProps = (props, {
|
|
|
47688
47997
|
return null;
|
|
47689
47998
|
},
|
|
47690
47999
|
click: e => {
|
|
47691
|
-
|
|
48000
|
+
// A click no mousedown came before (the keyboard, a label) is a
|
|
48001
|
+
// press the mousedown reaction never saw.
|
|
48002
|
+
if (actsOnMouseDown && e.detail !== 0) {
|
|
47692
48003
|
return null;
|
|
47693
48004
|
}
|
|
47694
48005
|
return {
|
|
@@ -48617,26 +48928,33 @@ const useControlUIState = (ref, uiStateInitial) => {
|
|
|
48617
48928
|
if (!element) {
|
|
48618
48929
|
return undefined;
|
|
48619
48930
|
}
|
|
48620
|
-
|
|
48621
|
-
// picker is a box holding an input) and the state is announced on the one
|
|
48622
|
-
// element that holds it. Same resolution every request goes through, see
|
|
48623
|
-
// ui_state_dom.js.
|
|
48624
|
-
const host = findControlHost(element) || element;
|
|
48625
|
-
// Asked once here rather than trusted from the props above: between the
|
|
48626
|
-
// first render and this effect the control has read its own value prop, its
|
|
48627
|
-
// defaultValue and any signal it was bound to, and settled which of them
|
|
48628
|
-
// wins — the answer to that is on the element, not in what we were handed.
|
|
48629
|
-
setUIState(getUIStateFromElement(host));
|
|
48630
|
-
const onUIStateChange = e => {
|
|
48631
|
-
setUIState(e.detail.value);
|
|
48632
|
-
};
|
|
48633
|
-
host.addEventListener("navi_ui_state_change", onUIStateChange);
|
|
48634
|
-
return () => {
|
|
48635
|
-
host.removeEventListener("navi_ui_state_change", onUIStateChange);
|
|
48636
|
-
};
|
|
48931
|
+
return subscribeToControlUIState(element, setUIState);
|
|
48637
48932
|
}, [ref]);
|
|
48638
48933
|
return uiState;
|
|
48639
48934
|
};
|
|
48935
|
+
/**
|
|
48936
|
+
* Calls `callback` with what the control `element` holds, right away and on
|
|
48937
|
+
* every change. Returns the teardown.
|
|
48938
|
+
*/
|
|
48939
|
+
const subscribeToControlUIState = (element, callback) => {
|
|
48940
|
+
// The host, not the box around it: a control is a whole little tree (a
|
|
48941
|
+
// picker is a box holding an input) and the state is announced on the one
|
|
48942
|
+
// element that holds it. Same resolution every request goes through, see
|
|
48943
|
+
// ui_state_dom.js.
|
|
48944
|
+
const host = findControlHost(element) || element;
|
|
48945
|
+
// Asked once here rather than trusted from props: by the time this runs the
|
|
48946
|
+
// control has read its own value prop, its defaultValue and any signal it
|
|
48947
|
+
// was bound to, and settled which of them wins — the answer to that is on
|
|
48948
|
+
// the element.
|
|
48949
|
+
callback(getUIStateFromElement(host));
|
|
48950
|
+
const onUIStateChange = e => {
|
|
48951
|
+
callback(e.detail.value);
|
|
48952
|
+
};
|
|
48953
|
+
host.addEventListener("navi_ui_state_change", onUIStateChange);
|
|
48954
|
+
return () => {
|
|
48955
|
+
host.removeEventListener("navi_ui_state_change", onUIStateChange);
|
|
48956
|
+
};
|
|
48957
|
+
};
|
|
48640
48958
|
|
|
48641
48959
|
/**
|
|
48642
48960
|
* Like `useControlProps` but also establishes a 1:1 facade sync between the
|
|
@@ -48932,7 +49250,8 @@ const useInteractiveProps = (props, {
|
|
|
48932
49250
|
const controlState = {
|
|
48933
49251
|
disabled: disabledResolved,
|
|
48934
49252
|
readOnly,
|
|
48935
|
-
required: requiredResolved
|
|
49253
|
+
required: requiredResolved,
|
|
49254
|
+
nameOn
|
|
48936
49255
|
};
|
|
48937
49256
|
for (const label of getAssociatedLabels(element)) {
|
|
48938
49257
|
label.dispatchEvent(new CustomEvent("navi_control_state", {
|
|
@@ -48953,6 +49272,22 @@ const useInteractiveProps = (props, {
|
|
|
48953
49272
|
unpublishControlStateToLabels(publishedIdRef.current);
|
|
48954
49273
|
};
|
|
48955
49274
|
}, []);
|
|
49275
|
+
// Named on its root, the control is named by its labels' ids: they drop
|
|
49276
|
+
// their `for` for it (see control_label_state.js). A name the caller gives
|
|
49277
|
+
// wins.
|
|
49278
|
+
const [labelledBy, setLabelledBy] = useState();
|
|
49279
|
+
const {
|
|
49280
|
+
id
|
|
49281
|
+
} = props;
|
|
49282
|
+
useLayoutEffect(() => {
|
|
49283
|
+
if (nameOn !== "root" || !id) {
|
|
49284
|
+
return undefined;
|
|
49285
|
+
}
|
|
49286
|
+
return subscribeToControlLabels(id, setLabelledBy);
|
|
49287
|
+
}, [id]);
|
|
49288
|
+
if (labelledBy && controlRootProps["aria-label"] === undefined && controlRootProps["aria-labelledby"] === undefined) {
|
|
49289
|
+
controlRootProps["aria-labelledby"] = labelledBy;
|
|
49290
|
+
}
|
|
48956
49291
|
}
|
|
48957
49292
|
{
|
|
48958
49293
|
const isCheckable = isCheckableInput(uiStateController.controlType, props.type);
|
|
@@ -53265,8 +53600,8 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
|
|
|
53265
53600
|
* those instead.
|
|
53266
53601
|
* @param {string} [props.value] - Value emitted by the control facade
|
|
53267
53602
|
* (`navi_value`); defaults to `href`.
|
|
53268
|
-
* @param {boolean} [props.current] - Forces the "current" state on
|
|
53269
|
-
* derived from the href/route
|
|
53603
|
+
* @param {boolean} [props.current] - Forces the "current" state: `true` on,
|
|
53604
|
+
* `false` off. Left out, it is derived from the href/route.
|
|
53270
53605
|
* @param {object|object[]} [props.currentExcept] -
|
|
53271
53606
|
* Route(s) inside this link's own that are NOT it: while one of them matches,
|
|
53272
53607
|
* the link is not current even though its route still is. A bar entry
|
|
@@ -53424,7 +53759,10 @@ const LinkWithRoute = ({
|
|
|
53424
53759
|
except: currentExcept,
|
|
53425
53760
|
also: currentAlso
|
|
53426
53761
|
});
|
|
53427
|
-
|
|
53762
|
+
// Always a boolean from here: LinkPlain takes a given `current` as the
|
|
53763
|
+
// answer, so an href matching the address cannot relight what
|
|
53764
|
+
// currentExcept turned off.
|
|
53765
|
+
const innerCurrent = current ?? linkMatching;
|
|
53428
53766
|
|
|
53429
53767
|
// What a childless link shows is LinkPlain's call (hrefFallback); a route
|
|
53430
53768
|
// only says it the way its pattern writes it, not as the resolved href.
|
|
@@ -53508,7 +53846,7 @@ const LinkPlain = props => {
|
|
|
53508
53846
|
} = getHrefTargetInfo(href, documentUrlSignal.peek());
|
|
53509
53847
|
// A tab that is a SLIDE is current when the container is on it — which the
|
|
53510
53848
|
// <Nav> around reads off that container, so nothing here has to be told.
|
|
53511
|
-
const innerCurrent = current
|
|
53849
|
+
const innerCurrent = current ?? (slide ? nav?.currentSlideArea === slide : isCurrent);
|
|
53512
53850
|
useReportCurrentToBinderItem(innerCurrent);
|
|
53513
53851
|
controlHostProps.basePseudoState = {
|
|
53514
53852
|
...basePseudoState,
|
|
@@ -58239,6 +58577,8 @@ const ButtonUI = props => {
|
|
|
58239
58577
|
// Where the href leads next to the page one is on (the target, the rel, the
|
|
58240
58578
|
// current state) is resolved above, by ButtonHrefResolver.
|
|
58241
58579
|
const as = href === undefined ? "button" : "a";
|
|
58580
|
+
// What `data-href-current` paints, said to assistive tech as a link says it.
|
|
58581
|
+
const ariaCurrent = as === "a" && props.pseudoState?.[":-navi-href-current"] ? "page" : undefined;
|
|
58242
58582
|
|
|
58243
58583
|
// Worn as an attribute, like a link's (see link_replace.js): read off the
|
|
58244
58584
|
// anchor by the click handler, off the source by --navi-nav-to.
|
|
@@ -58275,6 +58615,7 @@ const ButtonUI = props => {
|
|
|
58275
58615
|
// caller's props, so their own flex direction wins.
|
|
58276
58616
|
,
|
|
58277
58617
|
flex: icon ? "x" : undefined,
|
|
58618
|
+
"aria-current": ariaCurrent,
|
|
58278
58619
|
...buttonControlRootProps,
|
|
58279
58620
|
...buttonControlHostProps,
|
|
58280
58621
|
// eslint-disable-next-line react/no-children-prop
|
|
@@ -58627,7 +58968,10 @@ const ButtonHrefResolver = props => {
|
|
|
58627
58968
|
// Whether the href is the page one is on depends on the address, and re-renders
|
|
58628
58969
|
// the button only when it flips (see useHrefTargetFlag). Whether it leaves the
|
|
58629
58970
|
// site does not depend on the address.
|
|
58630
|
-
const ButtonWithHref =
|
|
58971
|
+
const ButtonWithHref = ({
|
|
58972
|
+
current,
|
|
58973
|
+
...props
|
|
58974
|
+
}) => {
|
|
58631
58975
|
const Next = useNextResolver();
|
|
58632
58976
|
const {
|
|
58633
58977
|
href,
|
|
@@ -58648,7 +58992,7 @@ const ButtonWithHref = props => {
|
|
|
58648
58992
|
// the state.
|
|
58649
58993
|
,
|
|
58650
58994
|
pseudoState: {
|
|
58651
|
-
":-navi-href-current": isCurrent,
|
|
58995
|
+
":-navi-href-current": current ?? isCurrent,
|
|
58652
58996
|
...pseudoState
|
|
58653
58997
|
}
|
|
58654
58998
|
});
|
|
@@ -58659,6 +59003,9 @@ const ButtonRouteResolver = props => {
|
|
|
58659
59003
|
if (props.route) {
|
|
58660
59004
|
return renderResolver(ButtonWithRoute, props);
|
|
58661
59005
|
}
|
|
59006
|
+
if ((props.currentExcept || props.currentAlso)) {
|
|
59007
|
+
console.warn(`currentExcept/currentAlso amend what the button's own route says about being current, so they need a "route" prop.`);
|
|
59008
|
+
}
|
|
58662
59009
|
return jsx(Next, {
|
|
58663
59010
|
...props
|
|
58664
59011
|
});
|
|
@@ -58670,6 +59017,9 @@ const ButtonWithRoute = props => {
|
|
|
58670
59017
|
const {
|
|
58671
59018
|
route,
|
|
58672
59019
|
routeParams,
|
|
59020
|
+
current,
|
|
59021
|
+
currentExcept,
|
|
59022
|
+
currentAlso,
|
|
58673
59023
|
children,
|
|
58674
59024
|
pseudoState,
|
|
58675
59025
|
href,
|
|
@@ -58680,9 +59030,18 @@ const ButtonWithRoute = props => {
|
|
|
58680
59030
|
if (href !== undefined) {
|
|
58681
59031
|
console.warn(`A button with a "route" ignores its "href" (${href}): the route builds the address.`);
|
|
58682
59032
|
}
|
|
59033
|
+
for (const amendingRoute of [currentExcept, currentAlso].flat()) {
|
|
59034
|
+
if (amendingRoute) {
|
|
59035
|
+
assertRoute(amendingRoute);
|
|
59036
|
+
}
|
|
59037
|
+
}
|
|
58683
59038
|
}
|
|
58684
59039
|
const url = route.buildUrl(routeParams);
|
|
58685
|
-
|
|
59040
|
+
// What a route Link reads, with the same amendments (see link.jsx).
|
|
59041
|
+
const linkMatching = useRouteCurrent(route, routeParams, {
|
|
59042
|
+
except: currentExcept,
|
|
59043
|
+
also: currentAlso
|
|
59044
|
+
});
|
|
58686
59045
|
|
|
58687
59046
|
// Merged into whatever the caller already holds: a button can be forced into
|
|
58688
59047
|
// a state for a demo and still learn its own current-ness from its route.
|
|
@@ -58690,7 +59049,7 @@ const ButtonWithRoute = props => {
|
|
|
58690
59049
|
href: url,
|
|
58691
59050
|
pseudoState: {
|
|
58692
59051
|
...pseudoState,
|
|
58693
|
-
":-navi-href-current": linkMatching
|
|
59052
|
+
":-navi-href-current": current ?? linkMatching
|
|
58694
59053
|
},
|
|
58695
59054
|
...rest,
|
|
58696
59055
|
children: children || route.buildRelativeUrl(routeParams)
|
|
@@ -58809,8 +59168,23 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
58809
59168
|
* actionStandalone?: boolean,
|
|
58810
59169
|
* actionAbortable?: boolean,
|
|
58811
59170
|
* optimistic?: boolean,
|
|
59171
|
+
* route?: object,
|
|
59172
|
+
* routeParams?: object,
|
|
59173
|
+
* current?: boolean,
|
|
59174
|
+
* currentExcept?: object | object[],
|
|
59175
|
+
* currentAlso?: object | object[],
|
|
58812
59176
|
* [key: string]: any,
|
|
58813
59177
|
* }>}
|
|
59178
|
+
* @param {object} [route] Where the press leads, as a `<Link route>` says it:
|
|
59179
|
+
* the button is drawn as an `<a>` to the route's url (with `routeParams`),
|
|
59180
|
+
* and says whether it is the page one is on — `[data-href-current]` for the
|
|
59181
|
+
* eye, `aria-current="page"` for assistive tech. A bar entry drawn as a
|
|
59182
|
+
* button for its press feedback.
|
|
59183
|
+
* @param {boolean} [current] Forces that "current" state: `true` on, `false`
|
|
59184
|
+
* off. Left out, it is read from the route (or the `href`).
|
|
59185
|
+
* @param {object|object[]} [currentExcept] Route(s) inside the button's own
|
|
59186
|
+
* that are NOT it, and `currentAlso` route(s) beside it that are: what they
|
|
59187
|
+
* mean on a `<Link>`.
|
|
58814
59188
|
* @param {boolean} [replace] Go where the press leads — an `href`, a
|
|
58815
59189
|
* `--navi-nav-to` command — by TAKING THE PLACE of the current history entry
|
|
58816
59190
|
* rather than stacking on it: what `<Link replace>` says, for a press drawn
|
|
@@ -60123,6 +60497,7 @@ const Label = props => {
|
|
|
60123
60497
|
const [readOnly, setReadOnly] = useState(false);
|
|
60124
60498
|
const [required, setRequired] = useState(false);
|
|
60125
60499
|
const [connected, setConnected] = useState(false);
|
|
60500
|
+
const [namesControlRoot, setNamesControlRoot] = useState(false);
|
|
60126
60501
|
// Set htmlFor only when we know the correct target id:
|
|
60127
60502
|
// - caller explicitly provided one (even undefined to opt out)
|
|
60128
60503
|
// - a parent Field provided one via ControlIdContext
|
|
@@ -60149,8 +60524,37 @@ const Label = props => {
|
|
|
60149
60524
|
setDisabled(Boolean(controlState?.disabled));
|
|
60150
60525
|
setReadOnly(Boolean(controlState?.readOnly));
|
|
60151
60526
|
setRequired(Boolean(controlState?.required));
|
|
60527
|
+
setNamesControlRoot(controlState?.nameOn === "root");
|
|
60152
60528
|
});
|
|
60153
60529
|
}, [htmlFor]);
|
|
60530
|
+
// A control whose role is on its root (a wheel) is named by this label's id
|
|
60531
|
+
// rather than by `for`, which can only point at its aria-hidden host (see
|
|
60532
|
+
// control_label_state.js).
|
|
60533
|
+
const labelId = props.id;
|
|
60534
|
+
useLayoutEffect(() => {
|
|
60535
|
+
if (!htmlFor || !namesControlRoot) {
|
|
60536
|
+
return undefined;
|
|
60537
|
+
}
|
|
60538
|
+
return publishLabelToControl(htmlFor, labelId);
|
|
60539
|
+
}, [htmlFor, namesControlRoot, labelId]);
|
|
60540
|
+
// …and without `for`, the press that focuses the control is this label's
|
|
60541
|
+
// to answer. A press on something interactive inside the label stays that
|
|
60542
|
+
// thing's, as the browser does for a label[for].
|
|
60543
|
+
const onClickFocusingControlRoot = e => {
|
|
60544
|
+
props.onClick?.(e);
|
|
60545
|
+
if (e.defaultPrevented) {
|
|
60546
|
+
return;
|
|
60547
|
+
}
|
|
60548
|
+
const interactive = e.target.closest("a[href], button, input, select, textarea, [tabindex]");
|
|
60549
|
+
if (interactive && e.currentTarget.contains(interactive)) {
|
|
60550
|
+
return;
|
|
60551
|
+
}
|
|
60552
|
+
const host = document.getElementById(htmlFor);
|
|
60553
|
+
const controlRoot = host && findControlRoot(host);
|
|
60554
|
+
if (controlRoot) {
|
|
60555
|
+
moveFocusTo(controlRoot);
|
|
60556
|
+
}
|
|
60557
|
+
};
|
|
60154
60558
|
const [messageProps, remainingProps] = extractMessageAndRemainingProps({
|
|
60155
60559
|
...props,
|
|
60156
60560
|
requiredIndicator: undefined
|
|
@@ -60167,6 +60571,8 @@ const Label = props => {
|
|
|
60167
60571
|
":read-only": readOnly
|
|
60168
60572
|
},
|
|
60169
60573
|
...remainingProps,
|
|
60574
|
+
htmlFor: namesControlRoot ? undefined : htmlFor,
|
|
60575
|
+
onClick: namesControlRoot ? onClickFocusingControlRoot : props.onClick,
|
|
60170
60576
|
onnavi_control_state: e => {
|
|
60171
60577
|
setConnected(true);
|
|
60172
60578
|
setDisabled(e.detail.disabled);
|
|
@@ -62763,36 +63169,61 @@ const Textarea = ({
|
|
|
62763
63169
|
* typed over how many the limit allows, the way Material writes it (just the
|
|
62764
63170
|
* count when there is no `maxLength`). Where it goes is the caller's call,
|
|
62765
63171
|
* which is why it is a separate component rather than something the textarea
|
|
62766
|
-
* draws: put it under the box, in a form footer, next to a label, and
|
|
62767
|
-
*
|
|
63172
|
+
* draws: put it under the box, in a form footer, next to a label, and point
|
|
63173
|
+
* it at the textarea with `for`.
|
|
62768
63174
|
*
|
|
62769
63175
|
* @type {import("preact").FunctionComponent<{
|
|
63176
|
+
* for?: string,
|
|
62770
63177
|
* value?: string,
|
|
62771
63178
|
* signal?: import("@preact/signals").Signal<string>,
|
|
62772
63179
|
* maxLength?: number,
|
|
62773
63180
|
* [key: string]: any,
|
|
62774
63181
|
* }>}
|
|
62775
|
-
* @param {string} [
|
|
62776
|
-
*
|
|
63182
|
+
* @param {string} [for] The `id` of the Textarea counted: the count reads
|
|
63183
|
+
* what it holds, so the field stays a plain one (`defaultValue={record.bio}`)
|
|
63184
|
+
* with no signal bound only for the counter's sake. The limit defaults to
|
|
63185
|
+
* that Textarea's `maxLength` (or `maxLengthGuard`).
|
|
63186
|
+
* @param {string} [value] The text being counted, when it is not read from a
|
|
63187
|
+
* Textarea. Say `signal` instead to subscribe the count to it.
|
|
62777
63188
|
* @param {number} [maxLength] The limit, shown after the count ("50/200").
|
|
62778
63189
|
* Without it the count stands alone.
|
|
62779
63190
|
*/
|
|
62780
63191
|
const TextareaCharCount = ({
|
|
63192
|
+
for: controlId,
|
|
62781
63193
|
value,
|
|
62782
63194
|
signal,
|
|
62783
63195
|
maxLength,
|
|
62784
63196
|
...rest
|
|
62785
63197
|
}) => {
|
|
62786
63198
|
import.meta.css = [css$J, "@jsenv/navi/src/control/input/textarea.jsx"];
|
|
62787
|
-
const
|
|
63199
|
+
const [controlText, controlMaxLength] = useTextOfControl(controlId);
|
|
63200
|
+
const resolvedValue = controlId ? controlText : signal ? signal.value : value;
|
|
62788
63201
|
const length = typeof resolvedValue === "string" ? resolvedValue.length : 0;
|
|
63202
|
+
const limit = maxLength ?? controlMaxLength;
|
|
62789
63203
|
return jsx(Box, {
|
|
62790
63204
|
as: "span",
|
|
62791
63205
|
baseClassName: "navi_textarea_char_count",
|
|
62792
63206
|
...rest,
|
|
62793
|
-
children:
|
|
63207
|
+
children: limit === undefined ? length : `${length}/${limit}`
|
|
62794
63208
|
});
|
|
62795
63209
|
};
|
|
63210
|
+
const useTextOfControl = controlId => {
|
|
63211
|
+
const [text, setText] = useState();
|
|
63212
|
+
const [maxLength, setMaxLength] = useState();
|
|
63213
|
+
useLayoutEffect(() => {
|
|
63214
|
+
if (!controlId) {
|
|
63215
|
+
return undefined;
|
|
63216
|
+
}
|
|
63217
|
+
const element = document.getElementById(controlId);
|
|
63218
|
+
if (!element) {
|
|
63219
|
+
return undefined;
|
|
63220
|
+
}
|
|
63221
|
+
const controlProps = getUIStateControllerById(controlId)?.props;
|
|
63222
|
+
setMaxLength(controlProps?.maxLength ?? controlProps?.maxLengthGuard);
|
|
63223
|
+
return subscribeToControlUIState(element, setText);
|
|
63224
|
+
}, [controlId]);
|
|
63225
|
+
return [text, maxLength];
|
|
63226
|
+
};
|
|
62796
63227
|
|
|
62797
63228
|
// `field-sizing: content` sizes the box from the value, and an empty field has
|
|
62798
63229
|
// none — the placeholder is text the browser refuses to make room for. So the
|
|
@@ -67150,6 +67581,11 @@ const Time = props => {
|
|
|
67150
67581
|
const {
|
|
67151
67582
|
type
|
|
67152
67583
|
} = props;
|
|
67584
|
+
if (props.value !== undefined && props.children === undefined) {
|
|
67585
|
+
// Otherwise drawn as the placeholder, a plausible screen with nothing wrong
|
|
67586
|
+
// on it.
|
|
67587
|
+
console.warn(`<Time> reads what it writes from its children, not from "value": <Time type="${type}">{value}</Time>.`);
|
|
67588
|
+
}
|
|
67153
67589
|
if (type === "date") {
|
|
67154
67590
|
return jsx(TimeDate, {
|
|
67155
67591
|
...props
|
|
@@ -67508,10 +67944,12 @@ const TimeHour = ({
|
|
|
67508
67944
|
children: String(children)
|
|
67509
67945
|
});
|
|
67510
67946
|
}
|
|
67947
|
+
const dateTime = formatClock(hours * 3600);
|
|
67511
67948
|
if (format === "timestring") {
|
|
67512
67949
|
const totalMinutes = Math.round(hours * 60);
|
|
67513
67950
|
const date = new Date(1970, 0, 1, Math.floor(totalMinutes / 60), totalMinutes % 60, 0);
|
|
67514
67951
|
return jsx(TimeText, {
|
|
67952
|
+
dateTime: dateTime,
|
|
67515
67953
|
...props,
|
|
67516
67954
|
children: formatTime(date, {
|
|
67517
67955
|
lang
|
|
@@ -67524,6 +67962,7 @@ const TimeHour = ({
|
|
|
67524
67962
|
forceUnit
|
|
67525
67963
|
});
|
|
67526
67964
|
return jsx(TimeText, {
|
|
67965
|
+
dateTime: dateTime,
|
|
67527
67966
|
...props,
|
|
67528
67967
|
children: text
|
|
67529
67968
|
});
|
|
@@ -71626,6 +72065,8 @@ const css$C = /* css */`
|
|
|
71626
72065
|
* actually closes — not preventable: it is the close happening, not a
|
|
71627
72066
|
* request. What refuses a close is navi's: a control inside still
|
|
71628
72067
|
* running its action (see popup_busy.js for the runs that let it through).
|
|
72068
|
+
* `event.detail.isCancel` says the close meant "revert", and
|
|
72069
|
+
* `event.detail.isConfirm` that a `--navi-confirm` said yes.
|
|
71629
72070
|
* @param {boolean|string|{id?: string, type?: "push"|"replace"}} [props.navState] -
|
|
71630
72071
|
* Keeps the open state in the history entry, so a screen left and come back
|
|
71631
72072
|
* to finds this popup as it was — open, and without an entrance playing: it
|
|
@@ -71864,6 +72305,7 @@ const UncontrolledDialog = props => {
|
|
|
71864
72305
|
onnavi_request_close: e => {
|
|
71865
72306
|
const closing = openController.requestClose(e, {
|
|
71866
72307
|
isCancel: e.detail?.isCancel,
|
|
72308
|
+
isConfirm: e.detail?.isConfirm,
|
|
71867
72309
|
requester: e.detail?.source
|
|
71868
72310
|
});
|
|
71869
72311
|
if (!closing) {
|
|
@@ -73712,6 +74154,8 @@ const css$B = /* css */`
|
|
|
73712
74154
|
* actually closes — not preventable: it is the close happening, not a
|
|
73713
74155
|
* request. What refuses a close is navi's: a control inside still
|
|
73714
74156
|
* running its action (see popup_busy.js for the runs that let it through).
|
|
74157
|
+
* `event.detail.isCancel` says the close meant "revert", and
|
|
74158
|
+
* `event.detail.isConfirm` that a `--navi-confirm` said yes.
|
|
73715
74159
|
* @param {boolean|string|{id?: string, type?: "push"|"replace"}} [props.navState] -
|
|
73716
74160
|
* Keeps the open state in the history entry, so a screen left and come back
|
|
73717
74161
|
* to finds this popup as it was — open, and without an entrance playing: it
|
|
@@ -73833,7 +74277,8 @@ const UncontrolledPopover = props => {
|
|
|
73833
74277
|
},
|
|
73834
74278
|
onnavi_request_close: e => {
|
|
73835
74279
|
const closing = openController.requestClose(e, {
|
|
73836
|
-
isCancel: e.detail?.isCancel
|
|
74280
|
+
isCancel: e.detail?.isCancel,
|
|
74281
|
+
isConfirm: e.detail?.isConfirm
|
|
73837
74282
|
});
|
|
73838
74283
|
if (!closing) {
|
|
73839
74284
|
// Said back to whoever asked: --navi-close:all stops climbing here.
|
|
@@ -75928,6 +76373,7 @@ const PickerCustom = props => {
|
|
|
75928
76373
|
allowed: () => {
|
|
75929
76374
|
const closing = requestClose(e, {
|
|
75930
76375
|
isCancel: e.detail.isCancel,
|
|
76376
|
+
isConfirm: e.detail.isConfirm,
|
|
75931
76377
|
isSend: e.detail.isSend
|
|
75932
76378
|
});
|
|
75933
76379
|
if (!closing) {
|
|
@@ -78982,6 +79428,11 @@ const css$v = /* css */`@property --list-group-label-height {
|
|
|
78982
79428
|
--list-item-background-color-highlight: #ffe066;
|
|
78983
79429
|
display: inline-block;
|
|
78984
79430
|
}
|
|
79431
|
+
|
|
79432
|
+
.navi_list_item_group[data-label-in-flow] > .navi_list_item_group_label {
|
|
79433
|
+
background-color: #0000;
|
|
79434
|
+
display: block;
|
|
79435
|
+
}
|
|
78985
79436
|
}
|
|
78986
79437
|
|
|
78987
79438
|
.navi_list_item_group_label {
|
|
@@ -79329,16 +79780,7 @@ const css$v = /* css */`@property --list-group-label-height {
|
|
|
79329
79780
|
min-width: 100%;
|
|
79330
79781
|
|
|
79331
79782
|
& .navi_list_item_group_label {
|
|
79332
|
-
top: var(--list-group-label-top, var(--x-list-group-label-top, 0px));
|
|
79333
|
-
z-index: var(--list-group-label-z-index);
|
|
79334
|
-
background-color: var(--list-group-label-background-color);
|
|
79335
79783
|
user-select: none;
|
|
79336
|
-
display: block;
|
|
79337
|
-
position: sticky;
|
|
79338
|
-
|
|
79339
|
-
&[navi-stuck] {
|
|
79340
|
-
z-index: var(--list-group-label-z-index-stuck);
|
|
79341
|
-
}
|
|
79342
79784
|
|
|
79343
79785
|
&[navi-default] {
|
|
79344
79786
|
color: light-dark(#888, #aaa);
|
|
@@ -79354,6 +79796,18 @@ const css$v = /* css */`@property --list-group-label-height {
|
|
|
79354
79796
|
}
|
|
79355
79797
|
}
|
|
79356
79798
|
|
|
79799
|
+
&:not([data-label-in-flow]) > .navi_list_item_group_label {
|
|
79800
|
+
top: var(--list-group-label-top, var(--x-list-group-label-top, 0px));
|
|
79801
|
+
z-index: var(--list-group-label-z-index);
|
|
79802
|
+
background-color: var(--list-group-label-background-color);
|
|
79803
|
+
display: block;
|
|
79804
|
+
position: sticky;
|
|
79805
|
+
|
|
79806
|
+
&[navi-stuck] {
|
|
79807
|
+
z-index: var(--list-group-label-z-index-stuck);
|
|
79808
|
+
}
|
|
79809
|
+
}
|
|
79810
|
+
|
|
79357
79811
|
& .navi_list_item_group_list {
|
|
79358
79812
|
--list-group-label-height: inherit;
|
|
79359
79813
|
flex-direction: column;
|
|
@@ -79372,6 +79826,20 @@ const css$v = /* css */`@property --list-group-label-height {
|
|
|
79372
79826
|
}
|
|
79373
79827
|
}
|
|
79374
79828
|
|
|
79829
|
+
&[data-label-in-flow] {
|
|
79830
|
+
& .navi_list_item_group_list .navi_list_item {
|
|
79831
|
+
scroll-margin-top: var(--x-list-scroll-spacing-top);
|
|
79832
|
+
}
|
|
79833
|
+
|
|
79834
|
+
& .navi_list_item_group_list > .navi_list_item:first-child {
|
|
79835
|
+
scroll-margin-top: calc(var(--x-list-scroll-spacing-top) + var(--list-group-label-height));
|
|
79836
|
+
}
|
|
79837
|
+
|
|
79838
|
+
&[data-continued] > .navi_list_item_group_label {
|
|
79839
|
+
visibility: hidden;
|
|
79840
|
+
}
|
|
79841
|
+
}
|
|
79842
|
+
|
|
79375
79843
|
&[data-hidden-while-empty]:not(:has([navi-list-item-real])) {
|
|
79376
79844
|
display: none;
|
|
79377
79845
|
}
|
|
@@ -80183,6 +80651,15 @@ const useListScrollSync = ({
|
|
|
80183
80651
|
// partly in view. After it, the quarter a window keeps behind the screen
|
|
80184
80652
|
// (see evaluateWindow), until the screen sizes it around that item.
|
|
80185
80653
|
const itemsAboveOpening = (openAt, windowSize) => {
|
|
80654
|
+
// An item placed in the middle of the view, or at its end, has the items
|
|
80655
|
+
// before it on screen from the first paint.
|
|
80656
|
+
const align = openAt && typeof openAt === "object" ? openAt.align : undefined;
|
|
80657
|
+
if (align === "center") {
|
|
80658
|
+
return Math.floor(windowSize / 2);
|
|
80659
|
+
}
|
|
80660
|
+
if (align === "end") {
|
|
80661
|
+
return windowSize - 1;
|
|
80662
|
+
}
|
|
80186
80663
|
if (stageRef.current !== "steady") {
|
|
80187
80664
|
return openAt && typeof openAt === "object" && openAt.offset > 0 ? 1 : 0;
|
|
80188
80665
|
}
|
|
@@ -80736,22 +81213,30 @@ const useListScrollSync = ({
|
|
|
80736
81213
|
return;
|
|
80737
81214
|
}
|
|
80738
81215
|
if (typeof openAt === "object" && openAt.id !== undefined) {
|
|
80739
|
-
placeItem(findItemElement(getListEl(), openAt.id), openAt.offset || 0);
|
|
81216
|
+
placeItem(findItemElement(getListEl(), openAt.id), openAt.offset || 0, openAt.align);
|
|
80740
81217
|
}
|
|
80741
81218
|
};
|
|
80742
81219
|
// An item is placed by measuring it where it is drawn — named, or at the
|
|
80743
81220
|
// place a number says — so it lands where it should whatever the items
|
|
80744
81221
|
// above it weigh, and wherever the list starts in the box that scrolls it.
|
|
80745
81222
|
// The window frames it from the first commit on (see holdWindow).
|
|
80746
|
-
const placeItem = (itemEl, offset) => {
|
|
81223
|
+
const placeItem = (itemEl, offset, align = "start") => {
|
|
80747
81224
|
if (!itemEl) {
|
|
80748
81225
|
return;
|
|
80749
81226
|
}
|
|
80750
81227
|
const scrollerEl = getScroller();
|
|
81228
|
+
// Measured in the scroller's own pixels, the ones a scroll is written in.
|
|
81229
|
+
const scale = readScrollerScale(scrollerEl, horizontal);
|
|
80751
81230
|
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
80752
81231
|
const itemRect = itemEl.getBoundingClientRect();
|
|
80753
|
-
const
|
|
80754
|
-
const
|
|
81232
|
+
const viewportSize = (horizontal ? viewportRect.width : viewportRect.height) / scale;
|
|
81233
|
+
const itemSize = (horizontal ? itemRect.width : itemRect.height) / scale;
|
|
81234
|
+
const scrollInset = getItemScrollInset(scrollerEl, itemEl, horizontal);
|
|
81235
|
+
// Centred in what the room at the start leaves of the view (a fixed bar,
|
|
81236
|
+
// a sticky header), not in the whole of it.
|
|
81237
|
+
const alignedOffset = align === "center" ? scrollInset + (viewportSize - scrollInset - itemSize) / 2 : align === "end" ? viewportSize - itemSize : scrollInset;
|
|
81238
|
+
const offsetWanted = resolveOpenOffset(alignedOffset + offset, viewportSize, itemSize);
|
|
81239
|
+
const offsetNow = (horizontal ? itemRect.left - viewportRect.left : itemRect.top - viewportRect.top) / scale;
|
|
80755
81240
|
const scrollNow = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
|
|
80756
81241
|
const delta = offsetNow - offsetWanted;
|
|
80757
81242
|
if (delta > -0.5 && delta < 0.5) {
|
|
@@ -81614,6 +82099,25 @@ const useListScrollSync = ({
|
|
|
81614
82099
|
// The band of the scroller the user actually sees. A page-level scroller is
|
|
81615
82100
|
// the viewport itself — its own box is the whole document, which says nothing
|
|
81616
82101
|
// about what is on screen.
|
|
82102
|
+
// A scroller drawn under a transform (a popup scaling in from its trigger)
|
|
82103
|
+
// shows a rect of another size than the box it scrolls: the ratio of the two
|
|
82104
|
+
// turns what the screen measures into the scroller's own pixels. A rounding
|
|
82105
|
+
// of the box's size is not a scale.
|
|
82106
|
+
const readScrollerScale = (scrollerEl, horizontal) => {
|
|
82107
|
+
if (scrollerEl === document.scrollingElement) {
|
|
82108
|
+
return 1;
|
|
82109
|
+
}
|
|
82110
|
+
const size = horizontal ? scrollerEl.offsetWidth : scrollerEl.offsetHeight;
|
|
82111
|
+
if (!size) {
|
|
82112
|
+
return 1;
|
|
82113
|
+
}
|
|
82114
|
+
const rect = scrollerEl.getBoundingClientRect();
|
|
82115
|
+
const scale = (horizontal ? rect.width : rect.height) / size;
|
|
82116
|
+
if (!scale || scale > 0.99 && scale < 1.01) {
|
|
82117
|
+
return 1;
|
|
82118
|
+
}
|
|
82119
|
+
return scale;
|
|
82120
|
+
};
|
|
81617
82121
|
const getScrollerViewportRect = scrollerEl => {
|
|
81618
82122
|
if (scrollerEl === document.scrollingElement) {
|
|
81619
82123
|
const width = document.documentElement.clientWidth;
|
|
@@ -81727,7 +82231,7 @@ const canScrollerScroll = (scrollerEl, axis) => {
|
|
|
81727
82231
|
// horizontally on mobile, where `overflow-x: clip` keeps that guarantee without
|
|
81728
82232
|
// making a scroll container, or to let a wide table scroll sideways, where the
|
|
81729
82233
|
// header cannot stick to both boxes.
|
|
81730
|
-
const STICKY_LIST_PART_SELECTOR = `.navi_list_item_header, .navi_list_item_footer, .navi_list_item_group_label`;
|
|
82234
|
+
const STICKY_LIST_PART_SELECTOR = `.navi_list_item_header, .navi_list_item_footer, .navi_list_item_group:not([data-label-in-flow]) > .navi_list_item_group_label`;
|
|
81731
82235
|
const useStickyScrollportWarning = (ref, scroller, getScroller, horizontal) => {
|
|
81732
82236
|
const doneRef = useRef(false);
|
|
81733
82237
|
useLayoutEffect(() => {
|
|
@@ -83838,6 +84342,7 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
83838
84342
|
* groupBy?: (item: any, index: number) => any,
|
|
83839
84343
|
* renderGroupLabel?: (item: any, index: number) => import("preact").ComponentChildren,
|
|
83840
84344
|
* groupLabelProps?: (item: any, index: number) => object,
|
|
84345
|
+
* groupLabelSticky?: boolean,
|
|
83841
84346
|
* pageSize?: number,
|
|
83842
84347
|
* memoryBudget?: number,
|
|
83843
84348
|
* renderSkeleton?: false | ((index: number) => import("preact").ComponentChildren),
|
|
@@ -83902,6 +84407,12 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
83902
84407
|
* its group (a day behind us, today, one ahead) rather than just naming it:
|
|
83903
84408
|
* the state then sits on the element the CSS styles, instead of being read
|
|
83904
84409
|
* back from a child.
|
|
84410
|
+
* @param {boolean} [props.groupLabelSticky=true]
|
|
84411
|
+
* `false`: the label stays in the flow, a boundary scrolled past with its
|
|
84412
|
+
* group instead of riding the top of the screen — a thread of days whose
|
|
84413
|
+
* cards must not slide under a band, a "past" line in a list the page
|
|
84414
|
+
* scrolls. It paints nothing of its own then: its display and background
|
|
84415
|
+
* are defaults a `groupLabelProps` class takes back.
|
|
83905
84416
|
* @param {number} [props.pageSize=100]
|
|
83906
84417
|
* How many items to ask for at a time. A turn of the wheel opens a hole three
|
|
83907
84418
|
* items wide; asking for exactly that would ask again at the next turn.
|
|
@@ -84086,6 +84597,7 @@ const useRunItems = (store, {
|
|
|
84086
84597
|
groupBy,
|
|
84087
84598
|
renderGroupLabel,
|
|
84088
84599
|
groupLabelProps,
|
|
84600
|
+
groupLabelSticky = true,
|
|
84089
84601
|
renderSkeleton,
|
|
84090
84602
|
renderError
|
|
84091
84603
|
}) => {
|
|
@@ -84345,6 +84857,7 @@ const useRunItems = (store, {
|
|
|
84345
84857
|
nodes.push(jsx(ListItemGroup, {
|
|
84346
84858
|
label: group.label,
|
|
84347
84859
|
labelProps: group.labelProps,
|
|
84860
|
+
labelSticky: groupLabelSticky,
|
|
84348
84861
|
continued: group.continued,
|
|
84349
84862
|
children: group.children
|
|
84350
84863
|
}, groupCount === 1 ? `${ownerId}_group_${group.key}` : `${ownerId}_group_${group.key}_${groupCount}`));
|
|
@@ -85349,6 +85862,7 @@ const useItemStore = ({
|
|
|
85349
85862
|
const ListItemGroup = ({
|
|
85350
85863
|
label,
|
|
85351
85864
|
labelProps,
|
|
85865
|
+
labelSticky = true,
|
|
85352
85866
|
hiddenWhileEmpty,
|
|
85353
85867
|
continued,
|
|
85354
85868
|
children,
|
|
@@ -85394,6 +85908,7 @@ const ListItemGroup = ({
|
|
|
85394
85908
|
role: "presentation",
|
|
85395
85909
|
"data-hidden-while-empty": hiddenWhileEmpty ? "" : undefined,
|
|
85396
85910
|
"data-continued": continued,
|
|
85911
|
+
"data-label-in-flow": labelSticky ? undefined : "",
|
|
85397
85912
|
children: [jsx("span", {
|
|
85398
85913
|
...labelRest,
|
|
85399
85914
|
ref: labelRef,
|
|
@@ -85461,8 +85976,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
85461
85976
|
* renderBudget?: number | string | {initial?: number | string, after?: number | string},
|
|
85462
85977
|
* virtualItemSize?: number,
|
|
85463
85978
|
* onListVisibleItemsChange?: (visibleItems: any[]) => void,
|
|
85464
|
-
* scrolled?: "start" | "end" | number | {id: string, offset?: number},
|
|
85465
|
-
* defaultScrolled?: "start" | "end" | number | {id: string, offset?: number},
|
|
85979
|
+
* scrolled?: "start" | "end" | number | {id: string, offset?: number, align?: "start" | "center" | "end"},
|
|
85980
|
+
* defaultScrolled?: "start" | "end" | number | {id: string, offset?: number, align?: "start" | "center" | "end"},
|
|
85466
85981
|
* onScrolledChange?: (scrolled: {id: string, index: number, offset: number}) => void,
|
|
85467
85982
|
* scroller?: "self" | "parent" | "document" | Element | {current: Element},
|
|
85468
85983
|
* hoverWhileScrolling?: boolean,
|
|
@@ -85566,7 +86081,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
85566
86081
|
* are skeletons, the others hold their room. `0` says the list is already
|
|
85567
86082
|
* known to be empty: the empty `fallback` shows right away rather than an
|
|
85568
86083
|
* empty frame.
|
|
85569
|
-
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.defaultScrolled="start"]
|
|
86084
|
+
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, align?: "start"|"center"|"end", visibleCount?: number}} [props.defaultScrolled="start"]
|
|
85570
86085
|
* Where the list opens, after which the user owns the scroll — unless it is
|
|
85571
86086
|
* being come back to (see `scrollResetOnNavigation`). `"end"` is a
|
|
85572
86087
|
* thread read backwards — the last items are the ones to show, and the ones
|
|
@@ -85581,10 +86096,13 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
85581
86096
|
* room has to be restated as a number by whoever asks. The `index` and
|
|
85582
86097
|
* `visibleCount` it also hands out say where to aim before the item is found,
|
|
85583
86098
|
* and how many items to draw before the first paint (see `renderBudget`).
|
|
86099
|
+
* `align: "center"` puts the item in the middle of the view instead (`"end"`:
|
|
86100
|
+
* at its far edge) — the chosen level of a horizontal scale opening on it —
|
|
86101
|
+
* and keeps it there while the box around it grows, until the user scrolls.
|
|
85584
86102
|
* A value that changes before anyone has moved the list is followed (a place
|
|
85585
86103
|
* read from a count that answered again): it is compared by value, so it can
|
|
85586
86104
|
* be computed at every render.
|
|
85587
|
-
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.scrolled]
|
|
86105
|
+
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, align?: "start"|"center"|"end", visibleCount?: number}} [props.scrolled]
|
|
85588
86106
|
* The same, but held: the list goes back there every time this changes, even
|
|
85589
86107
|
* after the user has scrolled — the caller owns where the list is (see
|
|
85590
86108
|
* `defaultScrolled` for the uncontrolled form, and `open`/`defaultOpen`
|
|
@@ -87086,6 +87604,7 @@ const css$r = /* css */`@layer navi {
|
|
|
87086
87604
|
--picker-font-size: var(--navi-control-font-size);
|
|
87087
87605
|
--picker-font-family: var(--navi-control-font-family, inherit);
|
|
87088
87606
|
--picker-loader-color: var(--navi-loader-color);
|
|
87607
|
+
--picker-cta-background-color: var(--navi-accent-color);
|
|
87089
87608
|
--picker-border-color: var(--navi-control-border-color);
|
|
87090
87609
|
--picker-background-color: white;
|
|
87091
87610
|
--picker-color: currentColor;
|
|
@@ -87184,7 +87703,7 @@ const css$r = /* css */`@layer navi {
|
|
|
87184
87703
|
line-height: inherit;
|
|
87185
87704
|
}
|
|
87186
87705
|
|
|
87187
|
-
.navi_picker[data-variant="button"] {
|
|
87706
|
+
.navi_picker:is([data-variant="button"], [data-variant="border"], [data-variant="discrete-border"]) {
|
|
87188
87707
|
--picker-padding-x-default: var(--navi-button-padding-x-default);
|
|
87189
87708
|
--picker-padding-y-default: var(--navi-button-padding-y-default);
|
|
87190
87709
|
--picker-align-x-default: center;
|
|
@@ -87202,6 +87721,49 @@ const css$r = /* css */`@layer navi {
|
|
|
87202
87721
|
--picker-text-align-default: center;
|
|
87203
87722
|
}
|
|
87204
87723
|
|
|
87724
|
+
.navi_picker[data-variant="border"] {
|
|
87725
|
+
--picker-background-color: transparent;
|
|
87726
|
+
--picker-background-color-hover: color-mix(in srgb,
|
|
87727
|
+
currentColor 8%,
|
|
87728
|
+
var(--picker-background-color));
|
|
87729
|
+
--picker-background-color-readonly: var(--picker-background-color);
|
|
87730
|
+
--picker-background-color-disabled: var(--picker-background-color);
|
|
87731
|
+
}
|
|
87732
|
+
|
|
87733
|
+
.navi_picker[data-variant="discrete-border"] {
|
|
87734
|
+
--picker-background-color: transparent;
|
|
87735
|
+
--picker-background-color-hover: var(--picker-background-color);
|
|
87736
|
+
--picker-background-color-readonly: var(--picker-background-color);
|
|
87737
|
+
--picker-background-color-disabled: var(--picker-background-color);
|
|
87738
|
+
}
|
|
87739
|
+
|
|
87740
|
+
.navi_picker[data-cta] {
|
|
87741
|
+
--picker-background-color: var(--picker-cta-background-color);
|
|
87742
|
+
--picker-border-color: var(--picker-cta-background-color);
|
|
87743
|
+
--picker-color: white;
|
|
87744
|
+
--picker-background-color-hover: color-mix(in srgb,
|
|
87745
|
+
var(--picker-background-color) 85%,
|
|
87746
|
+
white);
|
|
87747
|
+
--picker-border-color-hover: color-mix(in srgb,
|
|
87748
|
+
var(--picker-border-color) 85%,
|
|
87749
|
+
white);
|
|
87750
|
+
--picker-background-color-readonly: color-mix(in srgb,
|
|
87751
|
+
var(--picker-background-color) 50%,
|
|
87752
|
+
white);
|
|
87753
|
+
--picker-border-color-readonly: color-mix(in srgb,
|
|
87754
|
+
var(--picker-border-color) 50%,
|
|
87755
|
+
white);
|
|
87756
|
+
--picker-background-color-disabled: color-mix(in srgb,
|
|
87757
|
+
var(--picker-background-color) 40%,
|
|
87758
|
+
white);
|
|
87759
|
+
--picker-border-color-disabled: color-mix(in srgb,
|
|
87760
|
+
var(--picker-border-color) 40%,
|
|
87761
|
+
white);
|
|
87762
|
+
--picker-color-disabled: color-mix(in srgb,
|
|
87763
|
+
var(--picker-color) 60%,
|
|
87764
|
+
transparent);
|
|
87765
|
+
}
|
|
87766
|
+
|
|
87205
87767
|
.navi_picker[data-variant="text"] {
|
|
87206
87768
|
--picker-padding-x-default: 0;
|
|
87207
87769
|
--picker-padding-y-default: 0;
|
|
@@ -87482,6 +88044,18 @@ const css$r = /* css */`@layer navi {
|
|
|
87482
88044
|
--x-picker-cursor: default;
|
|
87483
88045
|
}
|
|
87484
88046
|
|
|
88047
|
+
&[data-variant="discrete-border"] {
|
|
88048
|
+
--x-picker-border-color: transparent;
|
|
88049
|
+
|
|
88050
|
+
&[data-hover] {
|
|
88051
|
+
--x-picker-border-color: var(--picker-border-color-hover);
|
|
88052
|
+
}
|
|
88053
|
+
|
|
88054
|
+
&[data-readonly], &[data-disabled] {
|
|
88055
|
+
--x-picker-border-color: transparent;
|
|
88056
|
+
}
|
|
88057
|
+
}
|
|
88058
|
+
|
|
87485
88059
|
&[data-callout] {
|
|
87486
88060
|
--x-picker-border-color: var(--callout-color);
|
|
87487
88061
|
}
|
|
@@ -87555,6 +88129,7 @@ const PickerButton = props => {
|
|
|
87555
88129
|
import.meta.css = [css$r, "@jsenv/navi/src/control/picker/picker.jsx"];
|
|
87556
88130
|
warnOnUnknownPickerType(props);
|
|
87557
88131
|
warnOnUIDrawnByNobody(props);
|
|
88132
|
+
warnOnMaxLinesNotClamped(props);
|
|
87558
88133
|
if (typeof props.maxLines === "string") {
|
|
87559
88134
|
props.maxLines = parseInt(props.maxLines);
|
|
87560
88135
|
}
|
|
@@ -87564,6 +88139,8 @@ const PickerButton = props => {
|
|
|
87564
88139
|
// The trigger is one icon — its box is that icon's, the way <Button icon>
|
|
87565
88140
|
// means it. A shape, so it composes with the variant painting around it.
|
|
87566
88141
|
icon,
|
|
88142
|
+
// A call to action, filled with the accent: what `cta` is on a Button.
|
|
88143
|
+
cta,
|
|
87567
88144
|
rightSlotIcon,
|
|
87568
88145
|
rightSlotIconSize = "inherit",
|
|
87569
88146
|
// What goes in the right slot as-is — no <Icon> around it, so a caller can
|
|
@@ -87642,7 +88219,9 @@ const PickerButton = props => {
|
|
|
87642
88219
|
const isBare = variant === "bare";
|
|
87643
88220
|
const bareChevron = isBare && chevron !== undefined ? chevron : undefined;
|
|
87644
88221
|
warnOnChevronOffBare(props, isBare);
|
|
87645
|
-
|
|
88222
|
+
// The Button's own looks: a label, no slot after it.
|
|
88223
|
+
const drawnAsButton = variant === "button" || variant === "border" || variant === "discrete-border";
|
|
88224
|
+
const hasRightSlot = !picksNothing && rightSlot !== null && !isIcon && variant !== "headless" && !drawnAsButton && variant !== "text" && (!isBare || bareChevron !== undefined) && ui !== "default";
|
|
87646
88225
|
warnOnClearableWithoutSlot(props, hasRightSlot);
|
|
87647
88226
|
// How far the press must follow a hanging chevron: its size, as a length the
|
|
87648
88227
|
// input's inset can add up. "inherit" is 1em of the picker's own font, which
|
|
@@ -87741,6 +88320,7 @@ const PickerButton = props => {
|
|
|
87741
88320
|
baseClassName: "navi_picker",
|
|
87742
88321
|
pseudoClasses: PICKER_BUTTON_PSEUDO_CLASSES,
|
|
87743
88322
|
"data-variant": variant,
|
|
88323
|
+
"data-cta": cta ? "" : undefined,
|
|
87744
88324
|
"data-icon": isIcon ? "" : undefined,
|
|
87745
88325
|
"data-chevron": hasRightSlot ? bareChevron : undefined,
|
|
87746
88326
|
"navi-picker": "",
|
|
@@ -87751,6 +88331,7 @@ const PickerButton = props => {
|
|
|
87751
88331
|
basePseudoState: basePseudoState,
|
|
87752
88332
|
styleCSSVars: PickerStyleCSSVars,
|
|
87753
88333
|
variant: undefined,
|
|
88334
|
+
cta: undefined,
|
|
87754
88335
|
icon: undefined
|
|
87755
88336
|
// The caller's own role goes onto the host, beside the name (see
|
|
87756
88337
|
// control_context.js): a role and a name on two different elements
|
|
@@ -87909,7 +88490,7 @@ const PickerButton = props => {
|
|
|
87909
88490
|
// caller is drawing there ("no filter", "anywhere"), so how it
|
|
87910
88491
|
// looks empty stays theirs (documented on the ui prop below).
|
|
87911
88492
|
,
|
|
87912
|
-
"navi-placeholder": (ui === undefined || pickerUIIsNaviOwn(ui)) &&
|
|
88493
|
+
"navi-placeholder": (ui === undefined || pickerUIIsNaviOwn(ui)) && !drawnAsButton && variant !== "text" && uiStateHoldsNothing(value) ? "" : undefined,
|
|
87913
88494
|
maxLines: maxLines,
|
|
87914
88495
|
children: jsx(PickerOwnContent, {
|
|
87915
88496
|
children: jsx(MaxLinesContext.Provider, {
|
|
@@ -88363,6 +88944,24 @@ const warnOnClearableWithoutSlot = (props, hasRightSlot) => {
|
|
|
88363
88944
|
}
|
|
88364
88945
|
console.warn(`[navi] <Picker ${cause} clearable> — that leaves no right slot, so no cross is drawn. ` + `Draw it where it belongs instead: <Picker ${cause} ui={<YourDrawing>… <Picker.Clear /></YourDrawing>}>. ` + `It is the same cross, and it takes itself away when there is nothing left to clear.`);
|
|
88365
88946
|
};
|
|
88947
|
+
|
|
88948
|
+
// A text or bare picker clamps nothing (see where maxLines is resolved), so a
|
|
88949
|
+
// `maxLines` written on one is dropped without a word.
|
|
88950
|
+
const maxLinesNotClampedWarnedSet = new Set();
|
|
88951
|
+
const warnOnMaxLinesNotClamped = props => {
|
|
88952
|
+
const {
|
|
88953
|
+
maxLines,
|
|
88954
|
+
variant
|
|
88955
|
+
} = props;
|
|
88956
|
+
if (maxLines === undefined || variant !== "text" && variant !== "bare") {
|
|
88957
|
+
return;
|
|
88958
|
+
}
|
|
88959
|
+
if (maxLinesNotClampedWarnedSet.has(variant)) {
|
|
88960
|
+
return;
|
|
88961
|
+
}
|
|
88962
|
+
maxLinesNotClampedWarnedSet.add(variant);
|
|
88963
|
+
console.warn(`[navi] <Picker variant="${variant}" maxLines={${maxLines}}> — a ${variant} picker clamps nothing: its drawing is the caller's own. ` + `Put maxLines on the Text in its ui: <Picker variant="${variant}" ui={<Text maxLines={${maxLines}}>…</Text>}>.`);
|
|
88964
|
+
};
|
|
88366
88965
|
let chevronOffBareWarned = false;
|
|
88367
88966
|
const warnOnChevronOffBare = (props, isBare) => {
|
|
88368
88967
|
const {
|
|
@@ -88463,7 +89062,8 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
|
|
|
88463
89062
|
* popupWidthFitContent?: boolean,
|
|
88464
89063
|
* loadingOutline?: "custom",
|
|
88465
89064
|
* loadingOutlineInset?: number,
|
|
88466
|
-
* variant?: "icon" | "circle" | "headless" | "discrete" | "button" | "text" | "bare" | "picker",
|
|
89065
|
+
* variant?: "icon" | "circle" | "headless" | "discrete" | "button" | "border" | "discrete-border" | "text" | "bare" | "picker",
|
|
89066
|
+
* cta?: boolean,
|
|
88467
89067
|
* icon?: boolean,
|
|
88468
89068
|
* alignX?: "start" | "center" | "end",
|
|
88469
89069
|
* alignY?: "start" | "center" | "end" | "stretch",
|
|
@@ -88728,10 +89328,13 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
|
|
|
88728
89328
|
* receives the cleared value like any other choice. A cut (Ctrl+X) on such a
|
|
88729
89329
|
* picker copies the value and leaves it in place: no clear without the
|
|
88730
89330
|
* question.
|
|
88731
|
-
* @param {"icon"|"headless"|"discrete"|"button"|"text"|"bare"|"picker"} [variant]
|
|
89331
|
+
* @param {"icon"|"headless"|"discrete"|"button"|"border"|"discrete-border"|"text"|"bare"|"picker"} [variant]
|
|
88732
89332
|
* How the trigger is drawn. `"button"` is a Button's drawing — surface,
|
|
88733
89333
|
* padding, a centered label, no chevron — and what a `type="confirm"` picker
|
|
88734
89334
|
* draws unless told otherwise: nothing is picked, the popup is a question.
|
|
89335
|
+
* `"border"` and `"discrete-border"` are the Button's two other looks, from
|
|
89336
|
+
* its own formulas: a picker swapped in for a Button that opened a popup
|
|
89337
|
+
* looks the same at rest, on hover and read-only.
|
|
88735
89338
|
* `"text"` is a word in a sentence, underlined with dots, no box and no
|
|
88736
89339
|
* slot: what `mode="callout"` uses for a callout on a term rather than on an
|
|
88737
89340
|
* icon (`"icon"` being its default, with an info icon in place of the
|
|
@@ -88755,6 +89358,9 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
|
|
|
88755
89358
|
* nearest positioned ancestor it happens to find. `"picker"` (or an explicit
|
|
88756
89359
|
* `variant={undefined}`) asks a confirm or callout picker for the field-like
|
|
88757
89360
|
* drawing every other picker has.
|
|
89361
|
+
* @param {boolean} [cta] A call to action: filled with the accent, its border
|
|
89362
|
+
* matching, as `cta` draws a Button — for a confirm picker that is a row's
|
|
89363
|
+
* main action.
|
|
88758
89364
|
* @param {"custom"} [loadingOutline] Who draws the loading outline while the
|
|
88759
89365
|
* action runs. Left unsaid, the picker does, around its own box. `"custom"`
|
|
88760
89366
|
* says someone else does — what it means on a `Button` too — for a picker
|
|
@@ -89264,8 +89870,13 @@ const TRAVELS_NONE = [];
|
|
|
89264
89870
|
* vertical?: boolean,
|
|
89265
89871
|
* previousLabel?: string,
|
|
89266
89872
|
* nextLabel?: string,
|
|
89873
|
+
* previousTestId?: string,
|
|
89874
|
+
* nextTestId?: string,
|
|
89267
89875
|
* [key: string]: any,
|
|
89268
89876
|
* }>}
|
|
89877
|
+
* @param {string} [previousTestId] The `data-testid` of the way out to the
|
|
89878
|
+
* previous value, which navi builds and the caller has no other way to name.
|
|
89879
|
+
* `nextTestId` is its twin.
|
|
89269
89880
|
* @param {any} [value] The value shown. Held from above: `uiAction` says when
|
|
89270
89881
|
* it should move. Say `signal` for a two-way binding instead, or
|
|
89271
89882
|
* `defaultValue` to let the spin hold the value itself.
|
|
@@ -89347,6 +89958,8 @@ const Spin = ({
|
|
|
89347
89958
|
maxLines,
|
|
89348
89959
|
previousLabel,
|
|
89349
89960
|
nextLabel,
|
|
89961
|
+
previousTestId,
|
|
89962
|
+
nextTestId,
|
|
89350
89963
|
...rest
|
|
89351
89964
|
}) => {
|
|
89352
89965
|
import.meta.css = [css$q, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
|
|
@@ -89658,6 +90271,7 @@ const Spin = ({
|
|
|
89658
90271
|
unavailable: !allowed || loadingResolved || readOnlyResolved,
|
|
89659
90272
|
unavailableMessage: () => wayOutMessage(allowed, isNext),
|
|
89660
90273
|
label: isNext ? nextLabel ?? spinI18n("spin.next") : previousLabel ?? spinI18n("spin.previous"),
|
|
90274
|
+
testId: isNext ? nextTestId : previousTestId,
|
|
89661
90275
|
onPress: e => {
|
|
89662
90276
|
focusMiddle();
|
|
89663
90277
|
if (editable) {
|
|
@@ -89888,6 +90502,7 @@ const WayOut = ({
|
|
|
89888
90502
|
unavailable,
|
|
89889
90503
|
unavailableMessage,
|
|
89890
90504
|
label,
|
|
90505
|
+
testId,
|
|
89891
90506
|
onPress,
|
|
89892
90507
|
onPointerDown,
|
|
89893
90508
|
children
|
|
@@ -89926,6 +90541,7 @@ const WayOut = ({
|
|
|
89926
90541
|
,
|
|
89927
90542
|
role: "button",
|
|
89928
90543
|
"aria-label": label,
|
|
90544
|
+
"data-testid": testId,
|
|
89929
90545
|
"aria-disabled": unavailable ? "true" : undefined,
|
|
89930
90546
|
"data-unavailable": unavailable ? "" : undefined
|
|
89931
90547
|
// At the chevron, not beside it: a callout aims its arrow at where the
|
|
@@ -98886,6 +99502,7 @@ const css$b = /* css */`@layer navi {
|
|
|
98886
99502
|
`;
|
|
98887
99503
|
const ButtonCopyToClipboard = ({
|
|
98888
99504
|
children,
|
|
99505
|
+
"aria-label": ariaLabel = buttonI18n("button.copy"),
|
|
98889
99506
|
...props
|
|
98890
99507
|
}) => {
|
|
98891
99508
|
import.meta.css = [css$b, "@jsenv/navi/src/control/input/button_copy_to_clipboard.jsx"];
|
|
@@ -98902,11 +99519,15 @@ const ButtonCopyToClipboard = ({
|
|
|
98902
99519
|
...props,
|
|
98903
99520
|
children: [jsx(Box, {
|
|
98904
99521
|
className: "navi_copied_notif",
|
|
98905
|
-
|
|
99522
|
+
role: "status",
|
|
98906
99523
|
opacity: copied ? 1 : 0,
|
|
98907
|
-
children: "
|
|
99524
|
+
children: copied ? buttonI18n("button.copied") : null
|
|
98908
99525
|
}), jsx(Button, {
|
|
98909
|
-
className: "navi_copy_button"
|
|
99526
|
+
className: "navi_copy_button"
|
|
99527
|
+
// The props land on the container; the name belongs to the button,
|
|
99528
|
+
// which shows only an icon.
|
|
99529
|
+
,
|
|
99530
|
+
"aria-label": ariaLabel,
|
|
98910
99531
|
icon: true,
|
|
98911
99532
|
revealOnInteraction: true,
|
|
98912
99533
|
square: true,
|