@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/jsenv_navi.js
CHANGED
|
@@ -953,6 +953,14 @@ const buttonI18n = /*#__PURE__*/ defineNaviMessages({
|
|
|
953
953
|
en: "Remove",
|
|
954
954
|
fr: "Retirer",
|
|
955
955
|
},
|
|
956
|
+
"button.copy": {
|
|
957
|
+
en: "Copy",
|
|
958
|
+
fr: "Copier",
|
|
959
|
+
},
|
|
960
|
+
"button.copied": {
|
|
961
|
+
en: "Copied!",
|
|
962
|
+
fr: "Copié !",
|
|
963
|
+
},
|
|
956
964
|
});
|
|
957
965
|
|
|
958
966
|
// Spin messages — the ends of what one steps through when the control holding
|
|
@@ -3610,7 +3618,6 @@ const mergeActionParams = (currentParams, newParams) => {
|
|
|
3610
3618
|
}
|
|
3611
3619
|
return mergeTwoJsValues(currentParams, newParams);
|
|
3612
3620
|
};
|
|
3613
|
-
|
|
3614
3621
|
const actionWeakMap = new WeakMap();
|
|
3615
3622
|
/**
|
|
3616
3623
|
* Wraps an async callback into an action: a question, its answer, and the
|
|
@@ -6959,6 +6966,32 @@ const notePageChange = () => {
|
|
|
6959
6966
|
|
|
6960
6967
|
const isRenderingPageChange = () => renderingPageChange;
|
|
6961
6968
|
|
|
6969
|
+
// A browser refusing storage (blocked site data, some private windows) throws
|
|
6970
|
+
// on the first touch of window.localStorage, and a full one throws on setItem.
|
|
6971
|
+
// Either way what was to be kept lives in memory for the session, which is all
|
|
6972
|
+
// a persisted value can fall back to — so every access goes through here.
|
|
6973
|
+
|
|
6974
|
+
const readLocalStorage = (key) => {
|
|
6975
|
+
try {
|
|
6976
|
+
const raw = window.localStorage.getItem(key);
|
|
6977
|
+
return raw === null ? undefined : raw;
|
|
6978
|
+
} catch {
|
|
6979
|
+
return undefined;
|
|
6980
|
+
}
|
|
6981
|
+
};
|
|
6982
|
+
|
|
6983
|
+
const writeLocalStorage = (key, value) => {
|
|
6984
|
+
try {
|
|
6985
|
+
if (value === undefined) {
|
|
6986
|
+
window.localStorage.removeItem(key);
|
|
6987
|
+
} else {
|
|
6988
|
+
window.localStorage.setItem(key, value);
|
|
6989
|
+
}
|
|
6990
|
+
} catch {
|
|
6991
|
+
// Full, or refused: the value lives for the session only.
|
|
6992
|
+
}
|
|
6993
|
+
};
|
|
6994
|
+
|
|
6962
6995
|
// Global signal registry for route template detection
|
|
6963
6996
|
const globalSignalRegistry = new Map();
|
|
6964
6997
|
|
|
@@ -6993,7 +7026,6 @@ const generateSignalId = () => {
|
|
|
6993
7026
|
const id = signalIdCounter++;
|
|
6994
7027
|
return id;
|
|
6995
7028
|
};
|
|
6996
|
-
|
|
6997
7029
|
/**
|
|
6998
7030
|
* Creates an advanced signal with dynamic default value, local storage persistence, and validation.
|
|
6999
7031
|
*
|
|
@@ -7189,27 +7221,19 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
7189
7221
|
fallback: () => getDefaultValue(false),
|
|
7190
7222
|
});
|
|
7191
7223
|
const readFromLocalStorage = persists
|
|
7192
|
-
? () =>
|
|
7193
|
-
const raw = window.localStorage.getItem(localStorageKey);
|
|
7194
|
-
if (raw === null) {
|
|
7195
|
-
return undefined;
|
|
7196
|
-
}
|
|
7197
|
-
return raw;
|
|
7198
|
-
}
|
|
7224
|
+
? () => readLocalStorage(localStorageKey)
|
|
7199
7225
|
: () => undefined;
|
|
7200
7226
|
const updateLocalStorage = persists
|
|
7201
7227
|
? () => {
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
window.localStorage.setItem(localStorageKey, localStorageValue);
|
|
7207
|
-
}
|
|
7228
|
+
writeLocalStorage(
|
|
7229
|
+
localStorageKey,
|
|
7230
|
+
validity.representations.localStorage.value,
|
|
7231
|
+
);
|
|
7208
7232
|
}
|
|
7209
7233
|
: () => {};
|
|
7210
7234
|
const removeFromLocalStorage = persists
|
|
7211
7235
|
? () => {
|
|
7212
|
-
|
|
7236
|
+
writeLocalStorage(localStorageKey, undefined);
|
|
7213
7237
|
}
|
|
7214
7238
|
: () => {};
|
|
7215
7239
|
|
|
@@ -9960,16 +9984,14 @@ let isUpdatingRoutesFromUrl = false;
|
|
|
9960
9984
|
* route(`/games/:gameId=${gamePageIdSignal}`, { dropSearchParams: ["v"] });
|
|
9961
9985
|
* ```
|
|
9962
9986
|
*/
|
|
9963
|
-
const route = (
|
|
9964
|
-
|
|
9965
|
-
{
|
|
9987
|
+
const route = (pattern, options = {}) => {
|
|
9988
|
+
const {
|
|
9966
9989
|
searchParams,
|
|
9967
9990
|
params,
|
|
9968
9991
|
redirectRoute,
|
|
9969
9992
|
redirectRouteParams,
|
|
9970
9993
|
dropSearchParams,
|
|
9971
|
-
} =
|
|
9972
|
-
) => {
|
|
9994
|
+
} = options;
|
|
9973
9995
|
const routePattern = createRoutePattern(pattern, { searchParams, params });
|
|
9974
9996
|
const { cleanPattern } = routePattern;
|
|
9975
9997
|
const [publishStatus, subscribeStatus] = createPubSub();
|
|
@@ -11642,7 +11664,6 @@ const activeRouteReadsSignal = computed(() => {
|
|
|
11642
11664
|
activeRouteReadsPrevious = reads;
|
|
11643
11665
|
return reads;
|
|
11644
11666
|
});
|
|
11645
|
-
|
|
11646
11667
|
/**
|
|
11647
11668
|
* Binds an action to a route: it runs when the route matches, with the params
|
|
11648
11669
|
* the effect reads off the address, runs again on a `reload()`, and is aborted
|
|
@@ -13481,6 +13502,12 @@ const stopLoad = (reason = "stopLoad() called") => {
|
|
|
13481
13502
|
* actions of the routes matching the url run again (see routeAction), one that
|
|
13482
13503
|
* failed included — what a "Retry" under a page that could not load calls.
|
|
13483
13504
|
* The address, the history and the scroll stay as they are.
|
|
13505
|
+
*
|
|
13506
|
+
* Route actions only: a component's `useAsyncData(fn)`, or an action no route
|
|
13507
|
+
* reads, is not asked again. Nor does it bring back code whose `import()`
|
|
13508
|
+
* failed — the document refuses that address again without a request — so
|
|
13509
|
+
* the way out of a failed import is a fresh document,
|
|
13510
|
+
* `window.location.reload()` (docs/dynamic_import.md).
|
|
13484
13511
|
*/
|
|
13485
13512
|
const reload = browserIntegration.reload;
|
|
13486
13513
|
/**
|
|
@@ -24338,16 +24365,11 @@ const arraySignalMembership = (...args) => {
|
|
|
24338
24365
|
};
|
|
24339
24366
|
|
|
24340
24367
|
const localStorageSignal = (key) => {
|
|
24341
|
-
const
|
|
24342
|
-
|
|
24343
|
-
const valueSignal = signal(initialValue === null ? undefined : initialValue);
|
|
24368
|
+
const valueSignal = signal(readLocalStorage(key));
|
|
24344
24369
|
effect(() => {
|
|
24345
24370
|
const value = valueSignal.value;
|
|
24346
|
-
|
|
24347
|
-
|
|
24348
|
-
} else {
|
|
24349
|
-
localStorage.setItem(key, value);
|
|
24350
|
-
}
|
|
24371
|
+
// Storage keeps strings: a null written as is would come back as "null".
|
|
24372
|
+
writeLocalStorage(key, value === null ? undefined : value);
|
|
24351
24373
|
});
|
|
24352
24374
|
|
|
24353
24375
|
return valueSignal;
|
|
@@ -25193,12 +25215,51 @@ const resolveRerunOn = (rerunOn, inheritedRerunOn = defaultRerunOn) => {
|
|
|
25193
25215
|
if (!rerunOn) {
|
|
25194
25216
|
return inheritedRerunOn;
|
|
25195
25217
|
}
|
|
25218
|
+
assertRerunOnCanFire(rerunOn);
|
|
25196
25219
|
return {
|
|
25197
25220
|
GET: rerunOn.GET ?? inheritedRerunOn.GET,
|
|
25198
25221
|
GET_MANY: rerunOn.GET_MANY ?? inheritedRerunOn.GET_MANY,
|
|
25199
25222
|
GET_RANGE: rerunOn.GET_RANGE ?? inheritedRerunOn.GET_RANGE,
|
|
25200
25223
|
};
|
|
25201
25224
|
};
|
|
25225
|
+
// What can never fire throws, as a dependency name does (see
|
|
25226
|
+
// resolveDependency): a rerun that never happens leaves a read on its old
|
|
25227
|
+
// value with nothing to say so.
|
|
25228
|
+
const RERUN_READS = ["GET", "GET_MANY", "GET_RANGE"];
|
|
25229
|
+
const RERUN_VERBS = ["POST", "PUT", "PATCH", "DELETE", "*"];
|
|
25230
|
+
const assertRerunOnCanFire = (rerunOn) => {
|
|
25231
|
+
for (const read of Object.keys(rerunOn)) {
|
|
25232
|
+
if (!RERUN_READS.includes(read)) {
|
|
25233
|
+
throw new Error(
|
|
25234
|
+
`rerunOn: "${read}" is not a read that reruns — rerunOn takes GET, GET_MANY and GET_RANGE`,
|
|
25235
|
+
);
|
|
25236
|
+
}
|
|
25237
|
+
const verbs = rerunOn[read];
|
|
25238
|
+
if (
|
|
25239
|
+
verbs === undefined ||
|
|
25240
|
+
verbs === null ||
|
|
25241
|
+
verbs === false ||
|
|
25242
|
+
verbs === "*"
|
|
25243
|
+
) {
|
|
25244
|
+
continue;
|
|
25245
|
+
}
|
|
25246
|
+
if (!Array.isArray(verbs)) {
|
|
25247
|
+
throw new Error(
|
|
25248
|
+
`rerunOn.${read}: ${JSON.stringify(verbs)} never fires — it takes false, "*" or a list of verbs`,
|
|
25249
|
+
);
|
|
25250
|
+
}
|
|
25251
|
+
for (const verb of verbs) {
|
|
25252
|
+
if (
|
|
25253
|
+
typeof verb !== "string" ||
|
|
25254
|
+
!RERUN_VERBS.includes(verb.toUpperCase())
|
|
25255
|
+
) {
|
|
25256
|
+
throw new Error(
|
|
25257
|
+
`rerunOn.${read}: ${JSON.stringify(verb)} never fires — rerunOn names verbs (POST, PUT, PATCH, DELETE or "*"): POST covers POST_MANY`,
|
|
25258
|
+
);
|
|
25259
|
+
}
|
|
25260
|
+
}
|
|
25261
|
+
}
|
|
25262
|
+
};
|
|
25202
25263
|
|
|
25203
25264
|
// What a `dependencies` entry names are callbacks, not verbs (`rerunOn` names
|
|
25204
25265
|
// verbs, where `PATCH` covers `PATCH_MANY`): one callback of a resource can be
|
|
@@ -26226,7 +26287,6 @@ const debug$2 = (...args) => {
|
|
|
26226
26287
|
return;
|
|
26227
26288
|
}
|
|
26228
26289
|
};
|
|
26229
|
-
|
|
26230
26290
|
/**
|
|
26231
26291
|
* Creates a reactive REST resource backed by a shared signal store.
|
|
26232
26292
|
* Returns a `stateFacade` exposing one action per REST callback provided
|
|
@@ -26285,9 +26345,8 @@ const debug$2 = (...args) => {
|
|
|
26285
26345
|
* DELETE: ({ id }) => fetchJson(`/users/${id}`, { method: "DELETE" }),
|
|
26286
26346
|
* });
|
|
26287
26347
|
*/
|
|
26288
|
-
const resource = (
|
|
26289
|
-
|
|
26290
|
-
{
|
|
26348
|
+
const resource = (name, restCallbacks = {}) => {
|
|
26349
|
+
const {
|
|
26291
26350
|
// configuration options
|
|
26292
26351
|
idKey = "id",
|
|
26293
26352
|
uniqueKeys = [],
|
|
@@ -26306,8 +26365,7 @@ const resource = (
|
|
|
26306
26365
|
PATCH_MANY,
|
|
26307
26366
|
DELETE,
|
|
26308
26367
|
DELETE_MANY,
|
|
26309
|
-
} =
|
|
26310
|
-
) => {
|
|
26368
|
+
} = restCallbacks;
|
|
26311
26369
|
const declarationSite = getDeclarationSite();
|
|
26312
26370
|
const setupCallbackSet = new Set();
|
|
26313
26371
|
const addItemSetup = (callback) => {
|
|
@@ -28199,8 +28257,8 @@ const valueInLocalStorage = (key, { type = "any" } = {}) => {
|
|
|
28199
28257
|
const converter = TYPE_CONVERTERS[type];
|
|
28200
28258
|
|
|
28201
28259
|
const get = () => {
|
|
28202
|
-
|
|
28203
|
-
if (valueInLocalStorage ===
|
|
28260
|
+
const valueInLocalStorage = readLocalStorage(key);
|
|
28261
|
+
if (valueInLocalStorage === undefined) {
|
|
28204
28262
|
return undefined;
|
|
28205
28263
|
}
|
|
28206
28264
|
let valueToReturn = valueInLocalStorage;
|
|
@@ -28224,7 +28282,7 @@ const valueInLocalStorage = (key, { type = "any" } = {}) => {
|
|
|
28224
28282
|
|
|
28225
28283
|
const set = (value) => {
|
|
28226
28284
|
if (value === undefined) {
|
|
28227
|
-
|
|
28285
|
+
writeLocalStorage(key, undefined);
|
|
28228
28286
|
return;
|
|
28229
28287
|
}
|
|
28230
28288
|
let valueToStore = value;
|
|
@@ -28232,10 +28290,10 @@ const valueInLocalStorage = (key, { type = "any" } = {}) => {
|
|
|
28232
28290
|
const valueEncoded = converter.encode(valueToStore);
|
|
28233
28291
|
valueToStore = valueEncoded;
|
|
28234
28292
|
}
|
|
28235
|
-
|
|
28293
|
+
writeLocalStorage(key, valueToStore);
|
|
28236
28294
|
};
|
|
28237
28295
|
const remove = () => {
|
|
28238
|
-
|
|
28296
|
+
writeLocalStorage(key, undefined);
|
|
28239
28297
|
};
|
|
28240
28298
|
|
|
28241
28299
|
return [get, set, remove];
|
|
@@ -28455,7 +28513,8 @@ const useForceRender = () => {
|
|
|
28455
28513
|
*
|
|
28456
28514
|
* @example <caption>Handle error inline</caption>
|
|
28457
28515
|
* const [user, , error] = useAsyncData(userAction, { error: true });
|
|
28458
|
-
*
|
|
28516
|
+
* // A failed refresh keeps `user`: only a first load has nothing else to show.
|
|
28517
|
+
* if (error && user === undefined) return <ErrorCard message={error.message} />;
|
|
28459
28518
|
*
|
|
28460
28519
|
* @example <caption>Say the failure over the rows already on screen</caption>
|
|
28461
28520
|
* const [items, , error, dismissError] = useAsyncData(listAction, { error: true });
|
|
@@ -40264,9 +40323,12 @@ const confirmCommand = (source, event) => {
|
|
|
40264
40323
|
event,
|
|
40265
40324
|
source: commandSource,
|
|
40266
40325
|
});
|
|
40326
|
+
// Carried by the close too: a Dialog or a Popover answers the yes in
|
|
40327
|
+
// its onClose (detail.isConfirm), the twin of isCancel.
|
|
40267
40328
|
return dispatchCustomEvent(target, "navi_request_close", {
|
|
40268
40329
|
event,
|
|
40269
40330
|
source: commandSource,
|
|
40331
|
+
isConfirm: true,
|
|
40270
40332
|
});
|
|
40271
40333
|
},
|
|
40272
40334
|
};
|
|
@@ -41994,6 +42056,14 @@ const useParallelGuard = (max) => {
|
|
|
41994
42056
|
* `getAssociatedLabels` in control_hooks.jsx. Both channels carry the same
|
|
41995
42057
|
* values and land on the same setters, so a control reachable through both is
|
|
41996
42058
|
* simply told twice.
|
|
42059
|
+
*
|
|
42060
|
+
* The link also runs the other way, for a control whose role is on its root
|
|
42061
|
+
* (`nameOn: "root"`, a wheel): a label[for] can only point at its host, an
|
|
42062
|
+
* aria-hidden carrier, and names nothing. That control says so in the state it
|
|
42063
|
+
* publishes; its label then drops `for` and publishes its own id under the
|
|
42064
|
+
* control's id instead, and the control names its root with the ids it finds
|
|
42065
|
+
* there (aria-labelledby). A label[for] kept as well would name the carrier
|
|
42066
|
+
* too, and a lookup by label would find two elements.
|
|
41997
42067
|
*/
|
|
41998
42068
|
|
|
41999
42069
|
const stateByControlId = new Map();
|
|
@@ -42046,6 +42116,68 @@ const subscribeToControlState = (controlId, callback) => {
|
|
|
42046
42116
|
};
|
|
42047
42117
|
};
|
|
42048
42118
|
|
|
42119
|
+
const labelIdSetByControlId = new Map();
|
|
42120
|
+
const labelCallbackSetByControlId = new Map();
|
|
42121
|
+
|
|
42122
|
+
const notifyLabelIds = (controlId) => {
|
|
42123
|
+
const callbackSet = labelCallbackSetByControlId.get(controlId);
|
|
42124
|
+
if (!callbackSet) {
|
|
42125
|
+
return;
|
|
42126
|
+
}
|
|
42127
|
+
const labelIds = readLabelIds(controlId);
|
|
42128
|
+
for (const callback of callbackSet) {
|
|
42129
|
+
callback(labelIds);
|
|
42130
|
+
}
|
|
42131
|
+
};
|
|
42132
|
+
// One string ("label_a label_b"), the shape aria-labelledby takes, and equal
|
|
42133
|
+
// from one call to the next while nothing changed.
|
|
42134
|
+
const readLabelIds = (controlId) => {
|
|
42135
|
+
const labelIdSet = labelIdSetByControlId.get(controlId);
|
|
42136
|
+
return labelIdSet ? Array.from(labelIdSet).join(" ") : undefined;
|
|
42137
|
+
};
|
|
42138
|
+
|
|
42139
|
+
/**
|
|
42140
|
+
* Publishes `labelId` as a label of the control identified by `controlId`.
|
|
42141
|
+
* Returns the teardown.
|
|
42142
|
+
*/
|
|
42143
|
+
const publishLabelToControl = (controlId, labelId) => {
|
|
42144
|
+
let labelIdSet = labelIdSetByControlId.get(controlId);
|
|
42145
|
+
if (!labelIdSet) {
|
|
42146
|
+
labelIdSet = new Set();
|
|
42147
|
+
labelIdSetByControlId.set(controlId, labelIdSet);
|
|
42148
|
+
}
|
|
42149
|
+
labelIdSet.add(labelId);
|
|
42150
|
+
notifyLabelIds(controlId);
|
|
42151
|
+
return () => {
|
|
42152
|
+
labelIdSet.delete(labelId);
|
|
42153
|
+
if (labelIdSet.size === 0) {
|
|
42154
|
+
labelIdSetByControlId.delete(controlId);
|
|
42155
|
+
}
|
|
42156
|
+
notifyLabelIds(controlId);
|
|
42157
|
+
};
|
|
42158
|
+
};
|
|
42159
|
+
|
|
42160
|
+
/**
|
|
42161
|
+
* Subscribes to the ids of the labels published for `controlId`. The callback
|
|
42162
|
+
* is called right away (with `undefined` when there is none), then on every
|
|
42163
|
+
* change. Returns the teardown.
|
|
42164
|
+
*/
|
|
42165
|
+
const subscribeToControlLabels = (controlId, callback) => {
|
|
42166
|
+
let callbackSet = labelCallbackSetByControlId.get(controlId);
|
|
42167
|
+
if (!callbackSet) {
|
|
42168
|
+
callbackSet = new Set();
|
|
42169
|
+
labelCallbackSetByControlId.set(controlId, callbackSet);
|
|
42170
|
+
}
|
|
42171
|
+
callbackSet.add(callback);
|
|
42172
|
+
callback(readLabelIds(controlId));
|
|
42173
|
+
return () => {
|
|
42174
|
+
callbackSet.delete(callback);
|
|
42175
|
+
if (callbackSet.size === 0) {
|
|
42176
|
+
labelCallbackSetByControlId.delete(controlId);
|
|
42177
|
+
}
|
|
42178
|
+
};
|
|
42179
|
+
};
|
|
42180
|
+
|
|
42049
42181
|
const FormContext = createContext();
|
|
42050
42182
|
|
|
42051
42183
|
/**
|
|
@@ -46342,17 +46474,11 @@ const useControlProps = (props, {
|
|
|
46342
46474
|
};
|
|
46343
46475
|
}
|
|
46344
46476
|
if (defaultAction === "activate") {
|
|
46345
|
-
// activating the control (e.g. space on a button/range)
|
|
46346
46477
|
return {
|
|
46347
46478
|
name: `keydown to activate`,
|
|
46348
46479
|
prevented: () => e.preventDefault(),
|
|
46349
46480
|
allowed: () => {
|
|
46350
46481
|
triggerUIAction(e);
|
|
46351
|
-
if (controlType === "button" && e.key === " ") {
|
|
46352
|
-
// prevent browser dispatching click
|
|
46353
|
-
// it could lead to duplicates as we explicitely handled the space to click here
|
|
46354
|
-
e.preventDefault();
|
|
46355
|
-
}
|
|
46356
46482
|
}
|
|
46357
46483
|
};
|
|
46358
46484
|
}
|
|
@@ -46489,7 +46615,15 @@ const useControlProps = (props, {
|
|
|
46489
46615
|
runWhenActionSucceeded(completion, deferredCommand);
|
|
46490
46616
|
};
|
|
46491
46617
|
return {
|
|
46492
|
-
keyDown:
|
|
46618
|
+
keyDown: e => {
|
|
46619
|
+
if (getKeyboardEventDefaultAction(e) === "activate") {
|
|
46620
|
+
// The browser presses the button itself — Enter clicks on
|
|
46621
|
+
// keydown, Space on keyup — and the click reaction below answers
|
|
46622
|
+
// that press, once: ui action, action and command together.
|
|
46623
|
+
return null;
|
|
46624
|
+
}
|
|
46625
|
+
return keyDownDefault(e);
|
|
46626
|
+
},
|
|
46493
46627
|
mouseDown: e => {
|
|
46494
46628
|
if (actsOnMouseDown) {
|
|
46495
46629
|
return {
|
|
@@ -46508,7 +46642,9 @@ const useControlProps = (props, {
|
|
|
46508
46642
|
return null;
|
|
46509
46643
|
},
|
|
46510
46644
|
click: e => {
|
|
46511
|
-
|
|
46645
|
+
// A click no mousedown came before (the keyboard, a label) is a
|
|
46646
|
+
// press the mousedown reaction never saw.
|
|
46647
|
+
if (actsOnMouseDown && e.detail !== 0) {
|
|
46512
46648
|
return null;
|
|
46513
46649
|
}
|
|
46514
46650
|
return {
|
|
@@ -47414,26 +47550,33 @@ const useControlUIState = (ref, uiStateInitial) => {
|
|
|
47414
47550
|
if (!element) {
|
|
47415
47551
|
return undefined;
|
|
47416
47552
|
}
|
|
47417
|
-
|
|
47418
|
-
// picker is a box holding an input) and the state is announced on the one
|
|
47419
|
-
// element that holds it. Same resolution every request goes through, see
|
|
47420
|
-
// ui_state_dom.js.
|
|
47421
|
-
const host = findControlHost(element) || element;
|
|
47422
|
-
// Asked once here rather than trusted from the props above: between the
|
|
47423
|
-
// first render and this effect the control has read its own value prop, its
|
|
47424
|
-
// defaultValue and any signal it was bound to, and settled which of them
|
|
47425
|
-
// wins — the answer to that is on the element, not in what we were handed.
|
|
47426
|
-
setUIState(getUIStateFromElement(host));
|
|
47427
|
-
const onUIStateChange = e => {
|
|
47428
|
-
setUIState(e.detail.value);
|
|
47429
|
-
};
|
|
47430
|
-
host.addEventListener("navi_ui_state_change", onUIStateChange);
|
|
47431
|
-
return () => {
|
|
47432
|
-
host.removeEventListener("navi_ui_state_change", onUIStateChange);
|
|
47433
|
-
};
|
|
47553
|
+
return subscribeToControlUIState(element, setUIState);
|
|
47434
47554
|
}, [ref]);
|
|
47435
47555
|
return uiState;
|
|
47436
47556
|
};
|
|
47557
|
+
/**
|
|
47558
|
+
* Calls `callback` with what the control `element` holds, right away and on
|
|
47559
|
+
* every change. Returns the teardown.
|
|
47560
|
+
*/
|
|
47561
|
+
const subscribeToControlUIState = (element, callback) => {
|
|
47562
|
+
// The host, not the box around it: a control is a whole little tree (a
|
|
47563
|
+
// picker is a box holding an input) and the state is announced on the one
|
|
47564
|
+
// element that holds it. Same resolution every request goes through, see
|
|
47565
|
+
// ui_state_dom.js.
|
|
47566
|
+
const host = findControlHost(element) || element;
|
|
47567
|
+
// Asked once here rather than trusted from props: by the time this runs the
|
|
47568
|
+
// control has read its own value prop, its defaultValue and any signal it
|
|
47569
|
+
// was bound to, and settled which of them wins — the answer to that is on
|
|
47570
|
+
// the element.
|
|
47571
|
+
callback(getUIStateFromElement(host));
|
|
47572
|
+
const onUIStateChange = e => {
|
|
47573
|
+
callback(e.detail.value);
|
|
47574
|
+
};
|
|
47575
|
+
host.addEventListener("navi_ui_state_change", onUIStateChange);
|
|
47576
|
+
return () => {
|
|
47577
|
+
host.removeEventListener("navi_ui_state_change", onUIStateChange);
|
|
47578
|
+
};
|
|
47579
|
+
};
|
|
47437
47580
|
|
|
47438
47581
|
/**
|
|
47439
47582
|
* Like `useControlProps` but also establishes a 1:1 facade sync between the
|
|
@@ -47729,7 +47872,8 @@ const useInteractiveProps = (props, {
|
|
|
47729
47872
|
const controlState = {
|
|
47730
47873
|
disabled: disabledResolved,
|
|
47731
47874
|
readOnly,
|
|
47732
|
-
required: requiredResolved
|
|
47875
|
+
required: requiredResolved,
|
|
47876
|
+
nameOn
|
|
47733
47877
|
};
|
|
47734
47878
|
for (const label of getAssociatedLabels(element)) {
|
|
47735
47879
|
label.dispatchEvent(new CustomEvent("navi_control_state", {
|
|
@@ -47750,6 +47894,22 @@ const useInteractiveProps = (props, {
|
|
|
47750
47894
|
unpublishControlStateToLabels(publishedIdRef.current);
|
|
47751
47895
|
};
|
|
47752
47896
|
}, []);
|
|
47897
|
+
// Named on its root, the control is named by its labels' ids: they drop
|
|
47898
|
+
// their `for` for it (see control_label_state.js). A name the caller gives
|
|
47899
|
+
// wins.
|
|
47900
|
+
const [labelledBy, setLabelledBy] = useState();
|
|
47901
|
+
const {
|
|
47902
|
+
id
|
|
47903
|
+
} = props;
|
|
47904
|
+
useLayoutEffect(() => {
|
|
47905
|
+
if (nameOn !== "root" || !id) {
|
|
47906
|
+
return undefined;
|
|
47907
|
+
}
|
|
47908
|
+
return subscribeToControlLabels(id, setLabelledBy);
|
|
47909
|
+
}, [id]);
|
|
47910
|
+
if (labelledBy && controlRootProps["aria-label"] === undefined && controlRootProps["aria-labelledby"] === undefined) {
|
|
47911
|
+
controlRootProps["aria-labelledby"] = labelledBy;
|
|
47912
|
+
}
|
|
47753
47913
|
}
|
|
47754
47914
|
{
|
|
47755
47915
|
const isCheckable = isCheckableInput(uiStateController.controlType, props.type);
|
|
@@ -52060,8 +52220,8 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
|
|
|
52060
52220
|
* those instead.
|
|
52061
52221
|
* @param {string} [props.value] - Value emitted by the control facade
|
|
52062
52222
|
* (`navi_value`); defaults to `href`.
|
|
52063
|
-
* @param {boolean} [props.current] - Forces the "current" state on
|
|
52064
|
-
* derived from the href/route
|
|
52223
|
+
* @param {boolean} [props.current] - Forces the "current" state: `true` on,
|
|
52224
|
+
* `false` off. Left out, it is derived from the href/route.
|
|
52065
52225
|
* @param {object|object[]} [props.currentExcept] -
|
|
52066
52226
|
* Route(s) inside this link's own that are NOT it: while one of them matches,
|
|
52067
52227
|
* the link is not current even though its route still is. A bar entry
|
|
@@ -52203,7 +52363,10 @@ const LinkWithRoute = ({
|
|
|
52203
52363
|
except: currentExcept,
|
|
52204
52364
|
also: currentAlso
|
|
52205
52365
|
});
|
|
52206
|
-
|
|
52366
|
+
// Always a boolean from here: LinkPlain takes a given `current` as the
|
|
52367
|
+
// answer, so an href matching the address cannot relight what
|
|
52368
|
+
// currentExcept turned off.
|
|
52369
|
+
const innerCurrent = current ?? linkMatching;
|
|
52207
52370
|
|
|
52208
52371
|
// What a childless link shows is LinkPlain's call (hrefFallback); a route
|
|
52209
52372
|
// only says it the way its pattern writes it, not as the resolved href.
|
|
@@ -52287,7 +52450,7 @@ const LinkPlain = props => {
|
|
|
52287
52450
|
} = getHrefTargetInfo(href, documentUrlSignal.peek());
|
|
52288
52451
|
// A tab that is a SLIDE is current when the container is on it — which the
|
|
52289
52452
|
// <Nav> around reads off that container, so nothing here has to be told.
|
|
52290
|
-
const innerCurrent = current
|
|
52453
|
+
const innerCurrent = current ?? (slide ? nav?.currentSlideArea === slide : isCurrent);
|
|
52291
52454
|
useReportCurrentToBinderItem(innerCurrent);
|
|
52292
52455
|
controlHostProps.basePseudoState = {
|
|
52293
52456
|
...basePseudoState,
|
|
@@ -57018,6 +57181,8 @@ const ButtonUI = props => {
|
|
|
57018
57181
|
// Where the href leads next to the page one is on (the target, the rel, the
|
|
57019
57182
|
// current state) is resolved above, by ButtonHrefResolver.
|
|
57020
57183
|
const as = href === undefined ? "button" : "a";
|
|
57184
|
+
// What `data-href-current` paints, said to assistive tech as a link says it.
|
|
57185
|
+
const ariaCurrent = as === "a" && props.pseudoState?.[":-navi-href-current"] ? "page" : undefined;
|
|
57021
57186
|
|
|
57022
57187
|
// Worn as an attribute, like a link's (see link_replace.js): read off the
|
|
57023
57188
|
// anchor by the click handler, off the source by --navi-nav-to.
|
|
@@ -57054,6 +57219,7 @@ const ButtonUI = props => {
|
|
|
57054
57219
|
// caller's props, so their own flex direction wins.
|
|
57055
57220
|
,
|
|
57056
57221
|
flex: icon ? "x" : undefined,
|
|
57222
|
+
"aria-current": ariaCurrent,
|
|
57057
57223
|
...buttonControlRootProps,
|
|
57058
57224
|
...buttonControlHostProps,
|
|
57059
57225
|
// eslint-disable-next-line react/no-children-prop
|
|
@@ -57406,7 +57572,10 @@ const ButtonHrefResolver = props => {
|
|
|
57406
57572
|
// Whether the href is the page one is on depends on the address, and re-renders
|
|
57407
57573
|
// the button only when it flips (see useHrefTargetFlag). Whether it leaves the
|
|
57408
57574
|
// site does not depend on the address.
|
|
57409
|
-
const ButtonWithHref =
|
|
57575
|
+
const ButtonWithHref = ({
|
|
57576
|
+
current,
|
|
57577
|
+
...props
|
|
57578
|
+
}) => {
|
|
57410
57579
|
const Next = useNextResolver();
|
|
57411
57580
|
const {
|
|
57412
57581
|
href,
|
|
@@ -57427,7 +57596,7 @@ const ButtonWithHref = props => {
|
|
|
57427
57596
|
// the state.
|
|
57428
57597
|
,
|
|
57429
57598
|
pseudoState: {
|
|
57430
|
-
":-navi-href-current": isCurrent,
|
|
57599
|
+
":-navi-href-current": current ?? isCurrent,
|
|
57431
57600
|
...pseudoState
|
|
57432
57601
|
}
|
|
57433
57602
|
});
|
|
@@ -57449,13 +57618,20 @@ const ButtonWithRoute = props => {
|
|
|
57449
57618
|
const {
|
|
57450
57619
|
route,
|
|
57451
57620
|
routeParams,
|
|
57621
|
+
current,
|
|
57622
|
+
currentExcept,
|
|
57623
|
+
currentAlso,
|
|
57452
57624
|
children,
|
|
57453
57625
|
pseudoState,
|
|
57454
57626
|
href,
|
|
57455
57627
|
...rest
|
|
57456
57628
|
} = props;
|
|
57457
57629
|
const url = route.buildUrl(routeParams);
|
|
57458
|
-
|
|
57630
|
+
// What a route Link reads, with the same amendments (see link.jsx).
|
|
57631
|
+
const linkMatching = useRouteCurrent(route, routeParams, {
|
|
57632
|
+
except: currentExcept,
|
|
57633
|
+
also: currentAlso
|
|
57634
|
+
});
|
|
57459
57635
|
|
|
57460
57636
|
// Merged into whatever the caller already holds: a button can be forced into
|
|
57461
57637
|
// a state for a demo and still learn its own current-ness from its route.
|
|
@@ -57463,7 +57639,7 @@ const ButtonWithRoute = props => {
|
|
|
57463
57639
|
href: url,
|
|
57464
57640
|
pseudoState: {
|
|
57465
57641
|
...pseudoState,
|
|
57466
|
-
":-navi-href-current": linkMatching
|
|
57642
|
+
":-navi-href-current": current ?? linkMatching
|
|
57467
57643
|
},
|
|
57468
57644
|
...rest,
|
|
57469
57645
|
children: children || route.buildRelativeUrl(routeParams)
|
|
@@ -57582,8 +57758,23 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
57582
57758
|
* actionStandalone?: boolean,
|
|
57583
57759
|
* actionAbortable?: boolean,
|
|
57584
57760
|
* optimistic?: boolean,
|
|
57761
|
+
* route?: object,
|
|
57762
|
+
* routeParams?: object,
|
|
57763
|
+
* current?: boolean,
|
|
57764
|
+
* currentExcept?: object | object[],
|
|
57765
|
+
* currentAlso?: object | object[],
|
|
57585
57766
|
* [key: string]: any,
|
|
57586
57767
|
* }>}
|
|
57768
|
+
* @param {object} [route] Where the press leads, as a `<Link route>` says it:
|
|
57769
|
+
* the button is drawn as an `<a>` to the route's url (with `routeParams`),
|
|
57770
|
+
* and says whether it is the page one is on — `[data-href-current]` for the
|
|
57771
|
+
* eye, `aria-current="page"` for assistive tech. A bar entry drawn as a
|
|
57772
|
+
* button for its press feedback.
|
|
57773
|
+
* @param {boolean} [current] Forces that "current" state: `true` on, `false`
|
|
57774
|
+
* off. Left out, it is read from the route (or the `href`).
|
|
57775
|
+
* @param {object|object[]} [currentExcept] Route(s) inside the button's own
|
|
57776
|
+
* that are NOT it, and `currentAlso` route(s) beside it that are: what they
|
|
57777
|
+
* mean on a `<Link>`.
|
|
57587
57778
|
* @param {boolean} [replace] Go where the press leads — an `href`, a
|
|
57588
57779
|
* `--navi-nav-to` command — by TAKING THE PLACE of the current history entry
|
|
57589
57780
|
* rather than stacking on it: what `<Link replace>` says, for a press drawn
|
|
@@ -58882,6 +59073,7 @@ const Label = props => {
|
|
|
58882
59073
|
const [readOnly, setReadOnly] = useState(false);
|
|
58883
59074
|
const [required, setRequired] = useState(false);
|
|
58884
59075
|
const [connected, setConnected] = useState(false);
|
|
59076
|
+
const [namesControlRoot, setNamesControlRoot] = useState(false);
|
|
58885
59077
|
// Set htmlFor only when we know the correct target id:
|
|
58886
59078
|
// - caller explicitly provided one (even undefined to opt out)
|
|
58887
59079
|
// - a parent Field provided one via ControlIdContext
|
|
@@ -58908,8 +59100,37 @@ const Label = props => {
|
|
|
58908
59100
|
setDisabled(Boolean(controlState?.disabled));
|
|
58909
59101
|
setReadOnly(Boolean(controlState?.readOnly));
|
|
58910
59102
|
setRequired(Boolean(controlState?.required));
|
|
59103
|
+
setNamesControlRoot(controlState?.nameOn === "root");
|
|
58911
59104
|
});
|
|
58912
59105
|
}, [htmlFor]);
|
|
59106
|
+
// A control whose role is on its root (a wheel) is named by this label's id
|
|
59107
|
+
// rather than by `for`, which can only point at its aria-hidden host (see
|
|
59108
|
+
// control_label_state.js).
|
|
59109
|
+
const labelId = props.id;
|
|
59110
|
+
useLayoutEffect(() => {
|
|
59111
|
+
if (!htmlFor || !namesControlRoot) {
|
|
59112
|
+
return undefined;
|
|
59113
|
+
}
|
|
59114
|
+
return publishLabelToControl(htmlFor, labelId);
|
|
59115
|
+
}, [htmlFor, namesControlRoot, labelId]);
|
|
59116
|
+
// …and without `for`, the press that focuses the control is this label's
|
|
59117
|
+
// to answer. A press on something interactive inside the label stays that
|
|
59118
|
+
// thing's, as the browser does for a label[for].
|
|
59119
|
+
const onClickFocusingControlRoot = e => {
|
|
59120
|
+
props.onClick?.(e);
|
|
59121
|
+
if (e.defaultPrevented) {
|
|
59122
|
+
return;
|
|
59123
|
+
}
|
|
59124
|
+
const interactive = e.target.closest("a[href], button, input, select, textarea, [tabindex]");
|
|
59125
|
+
if (interactive && e.currentTarget.contains(interactive)) {
|
|
59126
|
+
return;
|
|
59127
|
+
}
|
|
59128
|
+
const host = document.getElementById(htmlFor);
|
|
59129
|
+
const controlRoot = host && findControlRoot(host);
|
|
59130
|
+
if (controlRoot) {
|
|
59131
|
+
moveFocusTo(controlRoot);
|
|
59132
|
+
}
|
|
59133
|
+
};
|
|
58913
59134
|
const [messageProps, remainingProps] = extractMessageAndRemainingProps({
|
|
58914
59135
|
...props,
|
|
58915
59136
|
requiredIndicator: undefined
|
|
@@ -58926,6 +59147,8 @@ const Label = props => {
|
|
|
58926
59147
|
":read-only": readOnly
|
|
58927
59148
|
},
|
|
58928
59149
|
...remainingProps,
|
|
59150
|
+
htmlFor: namesControlRoot ? undefined : htmlFor,
|
|
59151
|
+
onClick: namesControlRoot ? onClickFocusingControlRoot : props.onClick,
|
|
58929
59152
|
onnavi_control_state: e => {
|
|
58930
59153
|
setConnected(true);
|
|
58931
59154
|
setDisabled(e.detail.disabled);
|
|
@@ -61522,36 +61745,61 @@ const Textarea = ({
|
|
|
61522
61745
|
* typed over how many the limit allows, the way Material writes it (just the
|
|
61523
61746
|
* count when there is no `maxLength`). Where it goes is the caller's call,
|
|
61524
61747
|
* which is why it is a separate component rather than something the textarea
|
|
61525
|
-
* draws: put it under the box, in a form footer, next to a label, and
|
|
61526
|
-
*
|
|
61748
|
+
* draws: put it under the box, in a form footer, next to a label, and point
|
|
61749
|
+
* it at the textarea with `for`.
|
|
61527
61750
|
*
|
|
61528
61751
|
* @type {import("preact").FunctionComponent<{
|
|
61752
|
+
* for?: string,
|
|
61529
61753
|
* value?: string,
|
|
61530
61754
|
* signal?: import("@preact/signals").Signal<string>,
|
|
61531
61755
|
* maxLength?: number,
|
|
61532
61756
|
* [key: string]: any,
|
|
61533
61757
|
* }>}
|
|
61534
|
-
* @param {string} [
|
|
61535
|
-
*
|
|
61758
|
+
* @param {string} [for] The `id` of the Textarea counted: the count reads
|
|
61759
|
+
* what it holds, so the field stays a plain one (`defaultValue={record.bio}`)
|
|
61760
|
+
* with no signal bound only for the counter's sake. The limit defaults to
|
|
61761
|
+
* that Textarea's `maxLength` (or `maxLengthGuard`).
|
|
61762
|
+
* @param {string} [value] The text being counted, when it is not read from a
|
|
61763
|
+
* Textarea. Say `signal` instead to subscribe the count to it.
|
|
61536
61764
|
* @param {number} [maxLength] The limit, shown after the count ("50/200").
|
|
61537
61765
|
* Without it the count stands alone.
|
|
61538
61766
|
*/
|
|
61539
61767
|
const TextareaCharCount = ({
|
|
61768
|
+
for: controlId,
|
|
61540
61769
|
value,
|
|
61541
61770
|
signal,
|
|
61542
61771
|
maxLength,
|
|
61543
61772
|
...rest
|
|
61544
61773
|
}) => {
|
|
61545
61774
|
import.meta.css = [css$J, "@jsenv/navi/src/control/input/textarea.jsx"];
|
|
61546
|
-
const
|
|
61775
|
+
const [controlText, controlMaxLength] = useTextOfControl(controlId);
|
|
61776
|
+
const resolvedValue = controlId ? controlText : signal ? signal.value : value;
|
|
61547
61777
|
const length = typeof resolvedValue === "string" ? resolvedValue.length : 0;
|
|
61778
|
+
const limit = maxLength ?? controlMaxLength;
|
|
61548
61779
|
return jsx(Box, {
|
|
61549
61780
|
as: "span",
|
|
61550
61781
|
baseClassName: "navi_textarea_char_count",
|
|
61551
61782
|
...rest,
|
|
61552
|
-
children:
|
|
61783
|
+
children: limit === undefined ? length : `${length}/${limit}`
|
|
61553
61784
|
});
|
|
61554
61785
|
};
|
|
61786
|
+
const useTextOfControl = controlId => {
|
|
61787
|
+
const [text, setText] = useState();
|
|
61788
|
+
const [maxLength, setMaxLength] = useState();
|
|
61789
|
+
useLayoutEffect(() => {
|
|
61790
|
+
if (!controlId) {
|
|
61791
|
+
return undefined;
|
|
61792
|
+
}
|
|
61793
|
+
const element = document.getElementById(controlId);
|
|
61794
|
+
if (!element) {
|
|
61795
|
+
return undefined;
|
|
61796
|
+
}
|
|
61797
|
+
const controlProps = getUIStateControllerById(controlId)?.props;
|
|
61798
|
+
setMaxLength(controlProps?.maxLength ?? controlProps?.maxLengthGuard);
|
|
61799
|
+
return subscribeToControlUIState(element, setText);
|
|
61800
|
+
}, [controlId]);
|
|
61801
|
+
return [text, maxLength];
|
|
61802
|
+
};
|
|
61555
61803
|
|
|
61556
61804
|
// `field-sizing: content` sizes the box from the value, and an empty field has
|
|
61557
61805
|
// none — the placeholder is text the browser refuses to make room for. So the
|
|
@@ -66217,10 +66465,12 @@ const TimeHour = ({
|
|
|
66217
66465
|
children: String(children)
|
|
66218
66466
|
});
|
|
66219
66467
|
}
|
|
66468
|
+
const dateTime = formatClock(hours * 3600);
|
|
66220
66469
|
if (format === "timestring") {
|
|
66221
66470
|
const totalMinutes = Math.round(hours * 60);
|
|
66222
66471
|
const date = new Date(1970, 0, 1, Math.floor(totalMinutes / 60), totalMinutes % 60, 0);
|
|
66223
66472
|
return jsx(TimeText, {
|
|
66473
|
+
dateTime: dateTime,
|
|
66224
66474
|
...props,
|
|
66225
66475
|
children: formatTime(date, {
|
|
66226
66476
|
lang
|
|
@@ -66233,6 +66483,7 @@ const TimeHour = ({
|
|
|
66233
66483
|
forceUnit
|
|
66234
66484
|
});
|
|
66235
66485
|
return jsx(TimeText, {
|
|
66486
|
+
dateTime: dateTime,
|
|
66236
66487
|
...props,
|
|
66237
66488
|
children: text
|
|
66238
66489
|
});
|
|
@@ -70300,6 +70551,8 @@ const css$C = /* css */`
|
|
|
70300
70551
|
* actually closes — not preventable: it is the close happening, not a
|
|
70301
70552
|
* request. What refuses a close is navi's: a control inside still
|
|
70302
70553
|
* running its action (see popup_busy.js for the runs that let it through).
|
|
70554
|
+
* `event.detail.isCancel` says the close meant "revert", and
|
|
70555
|
+
* `event.detail.isConfirm` that a `--navi-confirm` said yes.
|
|
70303
70556
|
* @param {boolean|string|{id?: string, type?: "push"|"replace"}} [props.navState] -
|
|
70304
70557
|
* Keeps the open state in the history entry, so a screen left and come back
|
|
70305
70558
|
* to finds this popup as it was — open, and without an entrance playing: it
|
|
@@ -70536,6 +70789,7 @@ const UncontrolledDialog = props => {
|
|
|
70536
70789
|
onnavi_request_close: e => {
|
|
70537
70790
|
const closing = openController.requestClose(e, {
|
|
70538
70791
|
isCancel: e.detail?.isCancel,
|
|
70792
|
+
isConfirm: e.detail?.isConfirm,
|
|
70539
70793
|
requester: e.detail?.source
|
|
70540
70794
|
});
|
|
70541
70795
|
if (!closing) {
|
|
@@ -72297,6 +72551,8 @@ const css$B = /* css */`
|
|
|
72297
72551
|
* actually closes — not preventable: it is the close happening, not a
|
|
72298
72552
|
* request. What refuses a close is navi's: a control inside still
|
|
72299
72553
|
* running its action (see popup_busy.js for the runs that let it through).
|
|
72554
|
+
* `event.detail.isCancel` says the close meant "revert", and
|
|
72555
|
+
* `event.detail.isConfirm` that a `--navi-confirm` said yes.
|
|
72300
72556
|
* @param {boolean|string|{id?: string, type?: "push"|"replace"}} [props.navState] -
|
|
72301
72557
|
* Keeps the open state in the history entry, so a screen left and come back
|
|
72302
72558
|
* to finds this popup as it was — open, and without an entrance playing: it
|
|
@@ -72416,7 +72672,8 @@ const UncontrolledPopover = props => {
|
|
|
72416
72672
|
},
|
|
72417
72673
|
onnavi_request_close: e => {
|
|
72418
72674
|
const closing = openController.requestClose(e, {
|
|
72419
|
-
isCancel: e.detail?.isCancel
|
|
72675
|
+
isCancel: e.detail?.isCancel,
|
|
72676
|
+
isConfirm: e.detail?.isConfirm
|
|
72420
72677
|
});
|
|
72421
72678
|
if (!closing) {
|
|
72422
72679
|
// Said back to whoever asked: --navi-close:all stops climbing here.
|
|
@@ -74488,6 +74745,7 @@ const PickerCustom = props => {
|
|
|
74488
74745
|
allowed: () => {
|
|
74489
74746
|
const closing = requestClose(e, {
|
|
74490
74747
|
isCancel: e.detail.isCancel,
|
|
74748
|
+
isConfirm: e.detail.isConfirm,
|
|
74491
74749
|
isSend: e.detail.isSend
|
|
74492
74750
|
});
|
|
74493
74751
|
if (!closing) {
|
|
@@ -77541,6 +77799,11 @@ const css$v = /* css */`@property --list-group-label-height {
|
|
|
77541
77799
|
--list-item-background-color-highlight: #ffe066;
|
|
77542
77800
|
display: inline-block;
|
|
77543
77801
|
}
|
|
77802
|
+
|
|
77803
|
+
.navi_list_item_group[data-label-in-flow] > .navi_list_item_group_label {
|
|
77804
|
+
background-color: #0000;
|
|
77805
|
+
display: block;
|
|
77806
|
+
}
|
|
77544
77807
|
}
|
|
77545
77808
|
|
|
77546
77809
|
.navi_list_item_group_label {
|
|
@@ -77888,16 +78151,7 @@ const css$v = /* css */`@property --list-group-label-height {
|
|
|
77888
78151
|
min-width: 100%;
|
|
77889
78152
|
|
|
77890
78153
|
& .navi_list_item_group_label {
|
|
77891
|
-
top: var(--list-group-label-top, var(--x-list-group-label-top, 0px));
|
|
77892
|
-
z-index: var(--list-group-label-z-index);
|
|
77893
|
-
background-color: var(--list-group-label-background-color);
|
|
77894
78154
|
user-select: none;
|
|
77895
|
-
display: block;
|
|
77896
|
-
position: sticky;
|
|
77897
|
-
|
|
77898
|
-
&[navi-stuck] {
|
|
77899
|
-
z-index: var(--list-group-label-z-index-stuck);
|
|
77900
|
-
}
|
|
77901
78155
|
|
|
77902
78156
|
&[navi-default] {
|
|
77903
78157
|
color: light-dark(#888, #aaa);
|
|
@@ -77913,6 +78167,18 @@ const css$v = /* css */`@property --list-group-label-height {
|
|
|
77913
78167
|
}
|
|
77914
78168
|
}
|
|
77915
78169
|
|
|
78170
|
+
&:not([data-label-in-flow]) > .navi_list_item_group_label {
|
|
78171
|
+
top: var(--list-group-label-top, var(--x-list-group-label-top, 0px));
|
|
78172
|
+
z-index: var(--list-group-label-z-index);
|
|
78173
|
+
background-color: var(--list-group-label-background-color);
|
|
78174
|
+
display: block;
|
|
78175
|
+
position: sticky;
|
|
78176
|
+
|
|
78177
|
+
&[navi-stuck] {
|
|
78178
|
+
z-index: var(--list-group-label-z-index-stuck);
|
|
78179
|
+
}
|
|
78180
|
+
}
|
|
78181
|
+
|
|
77916
78182
|
& .navi_list_item_group_list {
|
|
77917
78183
|
--list-group-label-height: inherit;
|
|
77918
78184
|
flex-direction: column;
|
|
@@ -77931,6 +78197,20 @@ const css$v = /* css */`@property --list-group-label-height {
|
|
|
77931
78197
|
}
|
|
77932
78198
|
}
|
|
77933
78199
|
|
|
78200
|
+
&[data-label-in-flow] {
|
|
78201
|
+
& .navi_list_item_group_list .navi_list_item {
|
|
78202
|
+
scroll-margin-top: var(--x-list-scroll-spacing-top);
|
|
78203
|
+
}
|
|
78204
|
+
|
|
78205
|
+
& .navi_list_item_group_list > .navi_list_item:first-child {
|
|
78206
|
+
scroll-margin-top: calc(var(--x-list-scroll-spacing-top) + var(--list-group-label-height));
|
|
78207
|
+
}
|
|
78208
|
+
|
|
78209
|
+
&[data-continued] > .navi_list_item_group_label {
|
|
78210
|
+
visibility: hidden;
|
|
78211
|
+
}
|
|
78212
|
+
}
|
|
78213
|
+
|
|
77934
78214
|
&[data-hidden-while-empty]:not(:has([navi-list-item-real])) {
|
|
77935
78215
|
display: none;
|
|
77936
78216
|
}
|
|
@@ -78703,6 +78983,15 @@ const useListScrollSync = ({
|
|
|
78703
78983
|
// partly in view. After it, the quarter a window keeps behind the screen
|
|
78704
78984
|
// (see evaluateWindow), until the screen sizes it around that item.
|
|
78705
78985
|
const itemsAboveOpening = (openAt, windowSize) => {
|
|
78986
|
+
// An item placed in the middle of the view, or at its end, has the items
|
|
78987
|
+
// before it on screen from the first paint.
|
|
78988
|
+
const align = openAt && typeof openAt === "object" ? openAt.align : undefined;
|
|
78989
|
+
if (align === "center") {
|
|
78990
|
+
return Math.floor(windowSize / 2);
|
|
78991
|
+
}
|
|
78992
|
+
if (align === "end") {
|
|
78993
|
+
return windowSize - 1;
|
|
78994
|
+
}
|
|
78706
78995
|
if (stageRef.current !== "steady") {
|
|
78707
78996
|
return openAt && typeof openAt === "object" && openAt.offset > 0 ? 1 : 0;
|
|
78708
78997
|
}
|
|
@@ -79243,22 +79532,30 @@ const useListScrollSync = ({
|
|
|
79243
79532
|
return;
|
|
79244
79533
|
}
|
|
79245
79534
|
if (typeof openAt === "object" && openAt.id !== undefined) {
|
|
79246
|
-
placeItem(findItemElement(getListEl(), openAt.id), openAt.offset || 0);
|
|
79535
|
+
placeItem(findItemElement(getListEl(), openAt.id), openAt.offset || 0, openAt.align);
|
|
79247
79536
|
}
|
|
79248
79537
|
};
|
|
79249
79538
|
// An item is placed by measuring it where it is drawn — named, or at the
|
|
79250
79539
|
// place a number says — so it lands where it should whatever the items
|
|
79251
79540
|
// above it weigh, and wherever the list starts in the box that scrolls it.
|
|
79252
79541
|
// The window frames it from the first commit on (see holdWindow).
|
|
79253
|
-
const placeItem = (itemEl, offset) => {
|
|
79542
|
+
const placeItem = (itemEl, offset, align = "start") => {
|
|
79254
79543
|
if (!itemEl) {
|
|
79255
79544
|
return;
|
|
79256
79545
|
}
|
|
79257
79546
|
const scrollerEl = getScroller();
|
|
79547
|
+
// Measured in the scroller's own pixels, the ones a scroll is written in.
|
|
79548
|
+
const scale = readScrollerScale(scrollerEl, horizontal);
|
|
79258
79549
|
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
79259
79550
|
const itemRect = itemEl.getBoundingClientRect();
|
|
79260
|
-
const
|
|
79261
|
-
const
|
|
79551
|
+
const viewportSize = (horizontal ? viewportRect.width : viewportRect.height) / scale;
|
|
79552
|
+
const itemSize = (horizontal ? itemRect.width : itemRect.height) / scale;
|
|
79553
|
+
const scrollInset = getItemScrollInset(scrollerEl, itemEl, horizontal);
|
|
79554
|
+
// Centred in what the room at the start leaves of the view (a fixed bar,
|
|
79555
|
+
// a sticky header), not in the whole of it.
|
|
79556
|
+
const alignedOffset = align === "center" ? scrollInset + (viewportSize - scrollInset - itemSize) / 2 : align === "end" ? viewportSize - itemSize : scrollInset;
|
|
79557
|
+
const offsetWanted = resolveOpenOffset(alignedOffset + offset, viewportSize, itemSize);
|
|
79558
|
+
const offsetNow = (horizontal ? itemRect.left - viewportRect.left : itemRect.top - viewportRect.top) / scale;
|
|
79262
79559
|
const scrollNow = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
|
|
79263
79560
|
const delta = offsetNow - offsetWanted;
|
|
79264
79561
|
if (delta > -0.5 && delta < 0.5) {
|
|
@@ -80111,6 +80408,25 @@ const useListScrollSync = ({
|
|
|
80111
80408
|
// The band of the scroller the user actually sees. A page-level scroller is
|
|
80112
80409
|
// the viewport itself — its own box is the whole document, which says nothing
|
|
80113
80410
|
// about what is on screen.
|
|
80411
|
+
// A scroller drawn under a transform (a popup scaling in from its trigger)
|
|
80412
|
+
// shows a rect of another size than the box it scrolls: the ratio of the two
|
|
80413
|
+
// turns what the screen measures into the scroller's own pixels. A rounding
|
|
80414
|
+
// of the box's size is not a scale.
|
|
80415
|
+
const readScrollerScale = (scrollerEl, horizontal) => {
|
|
80416
|
+
if (scrollerEl === document.scrollingElement) {
|
|
80417
|
+
return 1;
|
|
80418
|
+
}
|
|
80419
|
+
const size = horizontal ? scrollerEl.offsetWidth : scrollerEl.offsetHeight;
|
|
80420
|
+
if (!size) {
|
|
80421
|
+
return 1;
|
|
80422
|
+
}
|
|
80423
|
+
const rect = scrollerEl.getBoundingClientRect();
|
|
80424
|
+
const scale = (horizontal ? rect.width : rect.height) / size;
|
|
80425
|
+
if (!scale || scale > 0.99 && scale < 1.01) {
|
|
80426
|
+
return 1;
|
|
80427
|
+
}
|
|
80428
|
+
return scale;
|
|
80429
|
+
};
|
|
80114
80430
|
const getScrollerViewportRect = scrollerEl => {
|
|
80115
80431
|
if (scrollerEl === document.scrollingElement) {
|
|
80116
80432
|
const width = document.documentElement.clientWidth;
|
|
@@ -80185,7 +80501,7 @@ const canScrollerScroll = (scrollerEl, axis) => {
|
|
|
80185
80501
|
// horizontally on mobile, where `overflow-x: clip` keeps that guarantee without
|
|
80186
80502
|
// making a scroll container, or to let a wide table scroll sideways, where the
|
|
80187
80503
|
// header cannot stick to both boxes.
|
|
80188
|
-
const STICKY_LIST_PART_SELECTOR = `.navi_list_item_header, .navi_list_item_footer, .navi_list_item_group_label`;
|
|
80504
|
+
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`;
|
|
80189
80505
|
const useStickyScrollportWarning = (ref, scroller, getScroller, horizontal) => {
|
|
80190
80506
|
useRef(false);
|
|
80191
80507
|
useLayoutEffect(() => {
|
|
@@ -82215,6 +82531,7 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
82215
82531
|
* groupBy?: (item: any, index: number) => any,
|
|
82216
82532
|
* renderGroupLabel?: (item: any, index: number) => import("preact").ComponentChildren,
|
|
82217
82533
|
* groupLabelProps?: (item: any, index: number) => object,
|
|
82534
|
+
* groupLabelSticky?: boolean,
|
|
82218
82535
|
* pageSize?: number,
|
|
82219
82536
|
* memoryBudget?: number,
|
|
82220
82537
|
* renderSkeleton?: false | ((index: number) => import("preact").ComponentChildren),
|
|
@@ -82279,6 +82596,12 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
82279
82596
|
* its group (a day behind us, today, one ahead) rather than just naming it:
|
|
82280
82597
|
* the state then sits on the element the CSS styles, instead of being read
|
|
82281
82598
|
* back from a child.
|
|
82599
|
+
* @param {boolean} [props.groupLabelSticky=true]
|
|
82600
|
+
* `false`: the label stays in the flow, a boundary scrolled past with its
|
|
82601
|
+
* group instead of riding the top of the screen — a thread of days whose
|
|
82602
|
+
* cards must not slide under a band, a "past" line in a list the page
|
|
82603
|
+
* scrolls. It paints nothing of its own then: its display and background
|
|
82604
|
+
* are defaults a `groupLabelProps` class takes back.
|
|
82282
82605
|
* @param {number} [props.pageSize=100]
|
|
82283
82606
|
* How many items to ask for at a time. A turn of the wheel opens a hole three
|
|
82284
82607
|
* items wide; asking for exactly that would ask again at the next turn.
|
|
@@ -82463,6 +82786,7 @@ const useRunItems = (store, {
|
|
|
82463
82786
|
groupBy,
|
|
82464
82787
|
renderGroupLabel,
|
|
82465
82788
|
groupLabelProps,
|
|
82789
|
+
groupLabelSticky = true,
|
|
82466
82790
|
renderSkeleton,
|
|
82467
82791
|
renderError
|
|
82468
82792
|
}) => {
|
|
@@ -82722,6 +83046,7 @@ const useRunItems = (store, {
|
|
|
82722
83046
|
nodes.push(jsx(ListItemGroup, {
|
|
82723
83047
|
label: group.label,
|
|
82724
83048
|
labelProps: group.labelProps,
|
|
83049
|
+
labelSticky: groupLabelSticky,
|
|
82725
83050
|
continued: group.continued,
|
|
82726
83051
|
children: group.children
|
|
82727
83052
|
}, groupCount === 1 ? `${ownerId}_group_${group.key}` : `${ownerId}_group_${group.key}_${groupCount}`));
|
|
@@ -83717,6 +84042,7 @@ const useItemStore = ({
|
|
|
83717
84042
|
const ListItemGroup = ({
|
|
83718
84043
|
label,
|
|
83719
84044
|
labelProps,
|
|
84045
|
+
labelSticky = true,
|
|
83720
84046
|
hiddenWhileEmpty,
|
|
83721
84047
|
continued,
|
|
83722
84048
|
children,
|
|
@@ -83762,6 +84088,7 @@ const ListItemGroup = ({
|
|
|
83762
84088
|
role: "presentation",
|
|
83763
84089
|
"data-hidden-while-empty": hiddenWhileEmpty ? "" : undefined,
|
|
83764
84090
|
"data-continued": continued,
|
|
84091
|
+
"data-label-in-flow": labelSticky ? undefined : "",
|
|
83765
84092
|
children: [jsx("span", {
|
|
83766
84093
|
...labelRest,
|
|
83767
84094
|
ref: labelRef,
|
|
@@ -83829,8 +84156,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
83829
84156
|
* renderBudget?: number | string | {initial?: number | string, after?: number | string},
|
|
83830
84157
|
* virtualItemSize?: number,
|
|
83831
84158
|
* onListVisibleItemsChange?: (visibleItems: any[]) => void,
|
|
83832
|
-
* scrolled?: "start" | "end" | number | {id: string, offset?: number},
|
|
83833
|
-
* defaultScrolled?: "start" | "end" | number | {id: string, offset?: number},
|
|
84159
|
+
* scrolled?: "start" | "end" | number | {id: string, offset?: number, align?: "start" | "center" | "end"},
|
|
84160
|
+
* defaultScrolled?: "start" | "end" | number | {id: string, offset?: number, align?: "start" | "center" | "end"},
|
|
83834
84161
|
* onScrolledChange?: (scrolled: {id: string, index: number, offset: number}) => void,
|
|
83835
84162
|
* scroller?: "self" | "parent" | "document" | Element | {current: Element},
|
|
83836
84163
|
* hoverWhileScrolling?: boolean,
|
|
@@ -83934,7 +84261,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
83934
84261
|
* are skeletons, the others hold their room. `0` says the list is already
|
|
83935
84262
|
* known to be empty: the empty `fallback` shows right away rather than an
|
|
83936
84263
|
* empty frame.
|
|
83937
|
-
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.defaultScrolled="start"]
|
|
84264
|
+
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, align?: "start"|"center"|"end", visibleCount?: number}} [props.defaultScrolled="start"]
|
|
83938
84265
|
* Where the list opens, after which the user owns the scroll — unless it is
|
|
83939
84266
|
* being come back to (see `scrollResetOnNavigation`). `"end"` is a
|
|
83940
84267
|
* thread read backwards — the last items are the ones to show, and the ones
|
|
@@ -83949,10 +84276,13 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
83949
84276
|
* room has to be restated as a number by whoever asks. The `index` and
|
|
83950
84277
|
* `visibleCount` it also hands out say where to aim before the item is found,
|
|
83951
84278
|
* and how many items to draw before the first paint (see `renderBudget`).
|
|
84279
|
+
* `align: "center"` puts the item in the middle of the view instead (`"end"`:
|
|
84280
|
+
* at its far edge) — the chosen level of a horizontal scale opening on it —
|
|
84281
|
+
* and keeps it there while the box around it grows, until the user scrolls.
|
|
83952
84282
|
* A value that changes before anyone has moved the list is followed (a place
|
|
83953
84283
|
* read from a count that answered again): it is compared by value, so it can
|
|
83954
84284
|
* be computed at every render.
|
|
83955
|
-
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.scrolled]
|
|
84285
|
+
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, align?: "start"|"center"|"end", visibleCount?: number}} [props.scrolled]
|
|
83956
84286
|
* The same, but held: the list goes back there every time this changes, even
|
|
83957
84287
|
* after the user has scrolled — the caller owns where the list is (see
|
|
83958
84288
|
* `defaultScrolled` for the uncontrolled form, and `open`/`defaultOpen`
|
|
@@ -85454,6 +85784,7 @@ const css$r = /* css */`@layer navi {
|
|
|
85454
85784
|
--picker-font-size: var(--navi-control-font-size);
|
|
85455
85785
|
--picker-font-family: var(--navi-control-font-family, inherit);
|
|
85456
85786
|
--picker-loader-color: var(--navi-loader-color);
|
|
85787
|
+
--picker-cta-background-color: var(--navi-accent-color);
|
|
85457
85788
|
--picker-border-color: var(--navi-control-border-color);
|
|
85458
85789
|
--picker-background-color: white;
|
|
85459
85790
|
--picker-color: currentColor;
|
|
@@ -85552,7 +85883,7 @@ const css$r = /* css */`@layer navi {
|
|
|
85552
85883
|
line-height: inherit;
|
|
85553
85884
|
}
|
|
85554
85885
|
|
|
85555
|
-
.navi_picker[data-variant="button"] {
|
|
85886
|
+
.navi_picker:is([data-variant="button"], [data-variant="border"], [data-variant="discrete-border"]) {
|
|
85556
85887
|
--picker-padding-x-default: var(--navi-button-padding-x-default);
|
|
85557
85888
|
--picker-padding-y-default: var(--navi-button-padding-y-default);
|
|
85558
85889
|
--picker-align-x-default: center;
|
|
@@ -85570,6 +85901,49 @@ const css$r = /* css */`@layer navi {
|
|
|
85570
85901
|
--picker-text-align-default: center;
|
|
85571
85902
|
}
|
|
85572
85903
|
|
|
85904
|
+
.navi_picker[data-variant="border"] {
|
|
85905
|
+
--picker-background-color: transparent;
|
|
85906
|
+
--picker-background-color-hover: color-mix(in srgb,
|
|
85907
|
+
currentColor 8%,
|
|
85908
|
+
var(--picker-background-color));
|
|
85909
|
+
--picker-background-color-readonly: var(--picker-background-color);
|
|
85910
|
+
--picker-background-color-disabled: var(--picker-background-color);
|
|
85911
|
+
}
|
|
85912
|
+
|
|
85913
|
+
.navi_picker[data-variant="discrete-border"] {
|
|
85914
|
+
--picker-background-color: transparent;
|
|
85915
|
+
--picker-background-color-hover: var(--picker-background-color);
|
|
85916
|
+
--picker-background-color-readonly: var(--picker-background-color);
|
|
85917
|
+
--picker-background-color-disabled: var(--picker-background-color);
|
|
85918
|
+
}
|
|
85919
|
+
|
|
85920
|
+
.navi_picker[data-cta] {
|
|
85921
|
+
--picker-background-color: var(--picker-cta-background-color);
|
|
85922
|
+
--picker-border-color: var(--picker-cta-background-color);
|
|
85923
|
+
--picker-color: white;
|
|
85924
|
+
--picker-background-color-hover: color-mix(in srgb,
|
|
85925
|
+
var(--picker-background-color) 85%,
|
|
85926
|
+
white);
|
|
85927
|
+
--picker-border-color-hover: color-mix(in srgb,
|
|
85928
|
+
var(--picker-border-color) 85%,
|
|
85929
|
+
white);
|
|
85930
|
+
--picker-background-color-readonly: color-mix(in srgb,
|
|
85931
|
+
var(--picker-background-color) 50%,
|
|
85932
|
+
white);
|
|
85933
|
+
--picker-border-color-readonly: color-mix(in srgb,
|
|
85934
|
+
var(--picker-border-color) 50%,
|
|
85935
|
+
white);
|
|
85936
|
+
--picker-background-color-disabled: color-mix(in srgb,
|
|
85937
|
+
var(--picker-background-color) 40%,
|
|
85938
|
+
white);
|
|
85939
|
+
--picker-border-color-disabled: color-mix(in srgb,
|
|
85940
|
+
var(--picker-border-color) 40%,
|
|
85941
|
+
white);
|
|
85942
|
+
--picker-color-disabled: color-mix(in srgb,
|
|
85943
|
+
var(--picker-color) 60%,
|
|
85944
|
+
transparent);
|
|
85945
|
+
}
|
|
85946
|
+
|
|
85573
85947
|
.navi_picker[data-variant="text"] {
|
|
85574
85948
|
--picker-padding-x-default: 0;
|
|
85575
85949
|
--picker-padding-y-default: 0;
|
|
@@ -85850,6 +86224,18 @@ const css$r = /* css */`@layer navi {
|
|
|
85850
86224
|
--x-picker-cursor: default;
|
|
85851
86225
|
}
|
|
85852
86226
|
|
|
86227
|
+
&[data-variant="discrete-border"] {
|
|
86228
|
+
--x-picker-border-color: transparent;
|
|
86229
|
+
|
|
86230
|
+
&[data-hover] {
|
|
86231
|
+
--x-picker-border-color: var(--picker-border-color-hover);
|
|
86232
|
+
}
|
|
86233
|
+
|
|
86234
|
+
&[data-readonly], &[data-disabled] {
|
|
86235
|
+
--x-picker-border-color: transparent;
|
|
86236
|
+
}
|
|
86237
|
+
}
|
|
86238
|
+
|
|
85853
86239
|
&[data-callout] {
|
|
85854
86240
|
--x-picker-border-color: var(--callout-color);
|
|
85855
86241
|
}
|
|
@@ -85930,6 +86316,8 @@ const PickerButton = props => {
|
|
|
85930
86316
|
// The trigger is one icon — its box is that icon's, the way <Button icon>
|
|
85931
86317
|
// means it. A shape, so it composes with the variant painting around it.
|
|
85932
86318
|
icon,
|
|
86319
|
+
// A call to action, filled with the accent: what `cta` is on a Button.
|
|
86320
|
+
cta,
|
|
85933
86321
|
rightSlotIcon,
|
|
85934
86322
|
rightSlotIconSize = "inherit",
|
|
85935
86323
|
// What goes in the right slot as-is — no <Icon> around it, so a caller can
|
|
@@ -86007,7 +86395,9 @@ const PickerButton = props => {
|
|
|
86007
86395
|
// after it, before it, or out of the flow altogether.
|
|
86008
86396
|
const isBare = variant === "bare";
|
|
86009
86397
|
const bareChevron = isBare && chevron !== undefined ? chevron : undefined;
|
|
86010
|
-
|
|
86398
|
+
// The Button's own looks: a label, no slot after it.
|
|
86399
|
+
const drawnAsButton = variant === "button" || variant === "border" || variant === "discrete-border";
|
|
86400
|
+
const hasRightSlot = !picksNothing && rightSlot !== null && !isIcon && variant !== "headless" && !drawnAsButton && variant !== "text" && (!isBare || bareChevron !== undefined) && ui !== "default";
|
|
86011
86401
|
// How far the press must follow a hanging chevron: its size, as a length the
|
|
86012
86402
|
// input's inset can add up. "inherit" is 1em of the picker's own font, which
|
|
86013
86403
|
// the input shares (font: inherit), so the em the icon is drawn in and the
|
|
@@ -86105,6 +86495,7 @@ const PickerButton = props => {
|
|
|
86105
86495
|
baseClassName: "navi_picker",
|
|
86106
86496
|
pseudoClasses: PICKER_BUTTON_PSEUDO_CLASSES,
|
|
86107
86497
|
"data-variant": variant,
|
|
86498
|
+
"data-cta": cta ? "" : undefined,
|
|
86108
86499
|
"data-icon": isIcon ? "" : undefined,
|
|
86109
86500
|
"data-chevron": hasRightSlot ? bareChevron : undefined,
|
|
86110
86501
|
"navi-picker": "",
|
|
@@ -86115,6 +86506,7 @@ const PickerButton = props => {
|
|
|
86115
86506
|
basePseudoState: basePseudoState,
|
|
86116
86507
|
styleCSSVars: PickerStyleCSSVars,
|
|
86117
86508
|
variant: undefined,
|
|
86509
|
+
cta: undefined,
|
|
86118
86510
|
icon: undefined
|
|
86119
86511
|
// The caller's own role goes onto the host, beside the name (see
|
|
86120
86512
|
// control_context.js): a role and a name on two different elements
|
|
@@ -86273,7 +86665,7 @@ const PickerButton = props => {
|
|
|
86273
86665
|
// caller is drawing there ("no filter", "anywhere"), so how it
|
|
86274
86666
|
// looks empty stays theirs (documented on the ui prop below).
|
|
86275
86667
|
,
|
|
86276
|
-
"navi-placeholder": (ui === undefined || pickerUIIsNaviOwn(ui)) &&
|
|
86668
|
+
"navi-placeholder": (ui === undefined || pickerUIIsNaviOwn(ui)) && !drawnAsButton && variant !== "text" && uiStateHoldsNothing(value) ? "" : undefined,
|
|
86277
86669
|
maxLines: maxLines,
|
|
86278
86670
|
children: jsx(PickerOwnContent, {
|
|
86279
86671
|
children: jsx(MaxLinesContext.Provider, {
|
|
@@ -86729,7 +87121,8 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
|
|
|
86729
87121
|
* popupWidthFitContent?: boolean,
|
|
86730
87122
|
* loadingOutline?: "custom",
|
|
86731
87123
|
* loadingOutlineInset?: number,
|
|
86732
|
-
* variant?: "icon" | "circle" | "headless" | "discrete" | "button" | "text" | "bare" | "picker",
|
|
87124
|
+
* variant?: "icon" | "circle" | "headless" | "discrete" | "button" | "border" | "discrete-border" | "text" | "bare" | "picker",
|
|
87125
|
+
* cta?: boolean,
|
|
86733
87126
|
* icon?: boolean,
|
|
86734
87127
|
* alignX?: "start" | "center" | "end",
|
|
86735
87128
|
* alignY?: "start" | "center" | "end" | "stretch",
|
|
@@ -86994,10 +87387,13 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
|
|
|
86994
87387
|
* receives the cleared value like any other choice. A cut (Ctrl+X) on such a
|
|
86995
87388
|
* picker copies the value and leaves it in place: no clear without the
|
|
86996
87389
|
* question.
|
|
86997
|
-
* @param {"icon"|"headless"|"discrete"|"button"|"text"|"bare"|"picker"} [variant]
|
|
87390
|
+
* @param {"icon"|"headless"|"discrete"|"button"|"border"|"discrete-border"|"text"|"bare"|"picker"} [variant]
|
|
86998
87391
|
* How the trigger is drawn. `"button"` is a Button's drawing — surface,
|
|
86999
87392
|
* padding, a centered label, no chevron — and what a `type="confirm"` picker
|
|
87000
87393
|
* draws unless told otherwise: nothing is picked, the popup is a question.
|
|
87394
|
+
* `"border"` and `"discrete-border"` are the Button's two other looks, from
|
|
87395
|
+
* its own formulas: a picker swapped in for a Button that opened a popup
|
|
87396
|
+
* looks the same at rest, on hover and read-only.
|
|
87001
87397
|
* `"text"` is a word in a sentence, underlined with dots, no box and no
|
|
87002
87398
|
* slot: what `mode="callout"` uses for a callout on a term rather than on an
|
|
87003
87399
|
* icon (`"icon"` being its default, with an info icon in place of the
|
|
@@ -87021,6 +87417,9 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
|
|
|
87021
87417
|
* nearest positioned ancestor it happens to find. `"picker"` (or an explicit
|
|
87022
87418
|
* `variant={undefined}`) asks a confirm or callout picker for the field-like
|
|
87023
87419
|
* drawing every other picker has.
|
|
87420
|
+
* @param {boolean} [cta] A call to action: filled with the accent, its border
|
|
87421
|
+
* matching, as `cta` draws a Button — for a confirm picker that is a row's
|
|
87422
|
+
* main action.
|
|
87024
87423
|
* @param {"custom"} [loadingOutline] Who draws the loading outline while the
|
|
87025
87424
|
* action runs. Left unsaid, the picker does, around its own box. `"custom"`
|
|
87026
87425
|
* says someone else does — what it means on a `Button` too — for a picker
|
|
@@ -87530,8 +87929,13 @@ const TRAVELS_NONE = [];
|
|
|
87530
87929
|
* vertical?: boolean,
|
|
87531
87930
|
* previousLabel?: string,
|
|
87532
87931
|
* nextLabel?: string,
|
|
87932
|
+
* previousTestId?: string,
|
|
87933
|
+
* nextTestId?: string,
|
|
87533
87934
|
* [key: string]: any,
|
|
87534
87935
|
* }>}
|
|
87936
|
+
* @param {string} [previousTestId] The `data-testid` of the way out to the
|
|
87937
|
+
* previous value, which navi builds and the caller has no other way to name.
|
|
87938
|
+
* `nextTestId` is its twin.
|
|
87535
87939
|
* @param {any} [value] The value shown. Held from above: `uiAction` says when
|
|
87536
87940
|
* it should move. Say `signal` for a two-way binding instead, or
|
|
87537
87941
|
* `defaultValue` to let the spin hold the value itself.
|
|
@@ -87613,6 +88017,8 @@ const Spin = ({
|
|
|
87613
88017
|
maxLines,
|
|
87614
88018
|
previousLabel,
|
|
87615
88019
|
nextLabel,
|
|
88020
|
+
previousTestId,
|
|
88021
|
+
nextTestId,
|
|
87616
88022
|
...rest
|
|
87617
88023
|
}) => {
|
|
87618
88024
|
import.meta.css = [css$q, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
|
|
@@ -87924,6 +88330,7 @@ const Spin = ({
|
|
|
87924
88330
|
unavailable: !allowed || loadingResolved || readOnlyResolved,
|
|
87925
88331
|
unavailableMessage: () => wayOutMessage(allowed, isNext),
|
|
87926
88332
|
label: isNext ? nextLabel ?? spinI18n("spin.next") : previousLabel ?? spinI18n("spin.previous"),
|
|
88333
|
+
testId: isNext ? nextTestId : previousTestId,
|
|
87927
88334
|
onPress: e => {
|
|
87928
88335
|
focusMiddle();
|
|
87929
88336
|
if (editable) {
|
|
@@ -88154,6 +88561,7 @@ const WayOut = ({
|
|
|
88154
88561
|
unavailable,
|
|
88155
88562
|
unavailableMessage,
|
|
88156
88563
|
label,
|
|
88564
|
+
testId,
|
|
88157
88565
|
onPress,
|
|
88158
88566
|
onPointerDown,
|
|
88159
88567
|
children
|
|
@@ -88192,6 +88600,7 @@ const WayOut = ({
|
|
|
88192
88600
|
,
|
|
88193
88601
|
role: "button",
|
|
88194
88602
|
"aria-label": label,
|
|
88603
|
+
"data-testid": testId,
|
|
88195
88604
|
"aria-disabled": unavailable ? "true" : undefined,
|
|
88196
88605
|
"data-unavailable": unavailable ? "" : undefined
|
|
88197
88606
|
// At the chevron, not beside it: a callout aims its arrow at where the
|
|
@@ -97141,6 +97550,7 @@ const css$b = /* css */`@layer navi {
|
|
|
97141
97550
|
`;
|
|
97142
97551
|
const ButtonCopyToClipboard = ({
|
|
97143
97552
|
children,
|
|
97553
|
+
"aria-label": ariaLabel = buttonI18n("button.copy"),
|
|
97144
97554
|
...props
|
|
97145
97555
|
}) => {
|
|
97146
97556
|
import.meta.css = [css$b, "@jsenv/navi/src/control/input/button_copy_to_clipboard.jsx"];
|
|
@@ -97157,11 +97567,15 @@ const ButtonCopyToClipboard = ({
|
|
|
97157
97567
|
...props,
|
|
97158
97568
|
children: [jsx(Box, {
|
|
97159
97569
|
className: "navi_copied_notif",
|
|
97160
|
-
|
|
97570
|
+
role: "status",
|
|
97161
97571
|
opacity: copied ? 1 : 0,
|
|
97162
|
-
children: "
|
|
97572
|
+
children: copied ? buttonI18n("button.copied") : null
|
|
97163
97573
|
}), jsx(Button, {
|
|
97164
|
-
className: "navi_copy_button"
|
|
97574
|
+
className: "navi_copy_button"
|
|
97575
|
+
// The props land on the container; the name belongs to the button,
|
|
97576
|
+
// which shows only an icon.
|
|
97577
|
+
,
|
|
97578
|
+
"aria-label": ariaLabel,
|
|
97165
97579
|
icon: true,
|
|
97166
97580
|
revealOnInteraction: true,
|
|
97167
97581
|
square: true,
|