@jsenv/navi 0.29.102 → 0.29.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/jsenv_navi.js +1245 -463
- package/dist/jsenv_navi.js.map +97 -36
- package/dist/jsenv_navi_side_effects.js +32 -6
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/AI_INSTRUCTIONS.md +19 -3
- package/docs/badge_list.md +80 -0
- package/docs/control_value.md +50 -5
- package/docs/interactions.md +17 -6
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -1339,6 +1339,10 @@ naviI18n.addAll({
|
|
|
1339
1339
|
en: "More actions",
|
|
1340
1340
|
fr: "Autres actions",
|
|
1341
1341
|
},
|
|
1342
|
+
"button.remove": {
|
|
1343
|
+
en: "Remove",
|
|
1344
|
+
fr: "Retirer",
|
|
1345
|
+
},
|
|
1342
1346
|
});
|
|
1343
1347
|
|
|
1344
1348
|
// Default built-in translations — apps can override any key via add()
|
|
@@ -1638,6 +1642,14 @@ naviI18n.addAll({
|
|
|
1638
1642
|
fr: "Cette option n'est pas disponible.",
|
|
1639
1643
|
en: "This option is not available.",
|
|
1640
1644
|
},
|
|
1645
|
+
"constraint.readonly.selection": {
|
|
1646
|
+
fr: "La sélection ne peut plus être modifiée.",
|
|
1647
|
+
en: "This selection cannot be changed.",
|
|
1648
|
+
},
|
|
1649
|
+
"constraint.readonly.choice": {
|
|
1650
|
+
fr: "Ce choix ne peut plus être changé.",
|
|
1651
|
+
en: "This choice cannot be changed.",
|
|
1652
|
+
},
|
|
1641
1653
|
"constraint.readonly.item": {
|
|
1642
1654
|
fr: "Cet élément n'est pas disponible.",
|
|
1643
1655
|
en: "This item is not available.",
|
|
@@ -2114,6 +2126,276 @@ naviI18n.addAll({
|
|
|
2114
2126
|
|
|
2115
2127
|
const FormContext = createContext();
|
|
2116
2128
|
|
|
2129
|
+
const CONSTRAINT_ATTRIBUTE_SET = new Set();
|
|
2130
|
+
|
|
2131
|
+
const CONSTRAINT_NAME_TO_PROP = {
|
|
2132
|
+
disabled: "disabledMessage",
|
|
2133
|
+
required: "requiredMessage",
|
|
2134
|
+
pattern: "patternMessage",
|
|
2135
|
+
type_email: "typeMessage",
|
|
2136
|
+
type_number: "typeMessage",
|
|
2137
|
+
min_length: "minLengthMessage",
|
|
2138
|
+
max_length: "maxLengthMessage",
|
|
2139
|
+
min: "minMessage",
|
|
2140
|
+
max: "maxMessage",
|
|
2141
|
+
single_space: "singleSpaceMessage",
|
|
2142
|
+
same_as: "sameAsMessage",
|
|
2143
|
+
min_lower_letter: "minLowerLetterMessage",
|
|
2144
|
+
min_upper_letter: "minUpperLetterMessage",
|
|
2145
|
+
min_digit: "minDigitMessage",
|
|
2146
|
+
min_special_char: "minSpecialCharMessage",
|
|
2147
|
+
one_of: "oneOfMessage",
|
|
2148
|
+
readonly: "readOnlyMessage",
|
|
2149
|
+
available: "availableMessage",
|
|
2150
|
+
};
|
|
2151
|
+
|
|
2152
|
+
const CONSTRAINT_MESSAGE_PROP_NAME_SET = new Set(
|
|
2153
|
+
Object.values(CONSTRAINT_NAME_TO_PROP),
|
|
2154
|
+
);
|
|
2155
|
+
|
|
2156
|
+
const extractMessageAndRemainingProps = (props) => {
|
|
2157
|
+
const ownMessages = {};
|
|
2158
|
+
const remaining = {};
|
|
2159
|
+
const keyToVisit = new Set(Object.keys(props));
|
|
2160
|
+
for (const key of keyToVisit) {
|
|
2161
|
+
if (CONSTRAINT_MESSAGE_PROP_NAME_SET.has(key)) {
|
|
2162
|
+
ownMessages[key] = props[key];
|
|
2163
|
+
} else {
|
|
2164
|
+
remaining[key] = props[key];
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
return [ownMessages, remaining];
|
|
2168
|
+
};
|
|
2169
|
+
|
|
2170
|
+
const getConstraintMessage = (
|
|
2171
|
+
controller,
|
|
2172
|
+
constraint,
|
|
2173
|
+
generatedMessage,
|
|
2174
|
+
{ requester },
|
|
2175
|
+
) => {
|
|
2176
|
+
const { name: constraintName } = constraint;
|
|
2177
|
+
const propName = CONSTRAINT_NAME_TO_PROP[constraintName];
|
|
2178
|
+
|
|
2179
|
+
// 1. Search first on the requester (e.g. the <li> that was clicked),
|
|
2180
|
+
// then fall back to element (e.g. the hidden <input>).
|
|
2181
|
+
if (requester) {
|
|
2182
|
+
const requesterController = requester.__uiStateController__;
|
|
2183
|
+
if (requesterController && requesterController !== controller) {
|
|
2184
|
+
const requesterControllerMessage = requesterController.props[propName];
|
|
2185
|
+
if (requesterControllerMessage) {
|
|
2186
|
+
return {
|
|
2187
|
+
message: requesterControllerMessage,
|
|
2188
|
+
origin: "requester controller",
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
const controllerMessage = controller.props[propName];
|
|
2195
|
+
if (controllerMessage) {
|
|
2196
|
+
return {
|
|
2197
|
+
message: controllerMessage,
|
|
2198
|
+
origin: "controller",
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
return {
|
|
2203
|
+
message: generatedMessage,
|
|
2204
|
+
origin: "generated message",
|
|
2205
|
+
};
|
|
2206
|
+
};
|
|
2207
|
+
|
|
2208
|
+
// prop that we'll set on the control
|
|
2209
|
+
const CONTROL_ATTRIBUTE_SET = new Set([
|
|
2210
|
+
...CONSTRAINT_ATTRIBUTE_SET,
|
|
2211
|
+
|
|
2212
|
+
"ref",
|
|
2213
|
+
"children",
|
|
2214
|
+
"id",
|
|
2215
|
+
"name",
|
|
2216
|
+
"type",
|
|
2217
|
+
"value",
|
|
2218
|
+
"checked",
|
|
2219
|
+
"placeholder",
|
|
2220
|
+
"inputMode",
|
|
2221
|
+
"autoComplete",
|
|
2222
|
+
"spellcheck",
|
|
2223
|
+
"autoCorrect",
|
|
2224
|
+
"aria-controls",
|
|
2225
|
+
"tabIndex",
|
|
2226
|
+
"command",
|
|
2227
|
+
"commandFor",
|
|
2228
|
+
"command-value", // not standard but make sense, allow to give param to the command in question
|
|
2229
|
+
"list",
|
|
2230
|
+
|
|
2231
|
+
// "ui-action-target",
|
|
2232
|
+
"navi-input-type",
|
|
2233
|
+
"navi-value-pad",
|
|
2234
|
+
"navi-control-proxy-for",
|
|
2235
|
+
"navi-command-proxy-for",
|
|
2236
|
+
"navi-command-target",
|
|
2237
|
+
"onnavi_command",
|
|
2238
|
+
"onnavi_request_open",
|
|
2239
|
+
"onnavi_request_close",
|
|
2240
|
+
|
|
2241
|
+
"data-callout-arrow-x",
|
|
2242
|
+
"data-callout-point-to-border-box",
|
|
2243
|
+
"data-callout-point-to-content-box",
|
|
2244
|
+
"data-callout-viewport-spacing",
|
|
2245
|
+
"data-callout-position",
|
|
2246
|
+
"data-callout-position-fixed",
|
|
2247
|
+
|
|
2248
|
+
"data-testid", // playwright, cypress
|
|
2249
|
+
"data-separator", // used by InputGroup paste-to-fill
|
|
2250
|
+
]);
|
|
2251
|
+
// prop concerning control but that won't end up in the DOM if not inside CONTROL_ATTRIBUTE_SET
|
|
2252
|
+
const CONTROL_PROP_SET = new Set([
|
|
2253
|
+
...CONTROL_ATTRIBUTE_SET,
|
|
2254
|
+
...CONSTRAINT_MESSAGE_PROP_NAME_SET,
|
|
2255
|
+
|
|
2256
|
+
"action",
|
|
2257
|
+
"confirm",
|
|
2258
|
+
"confirmPopupContent",
|
|
2259
|
+
"actionEvent",
|
|
2260
|
+
"actionAfterChange",
|
|
2261
|
+
"actionOnMouseDown",
|
|
2262
|
+
"actionDebounce",
|
|
2263
|
+
// A signal bound two-way to the control: its value seeds the control's state and
|
|
2264
|
+
// is written back on every uiAction. Precludes value/checked (see createControlInfo).
|
|
2265
|
+
"signal",
|
|
2266
|
+
"defaultValue",
|
|
2267
|
+
"defaultChecked",
|
|
2268
|
+
"readOnly", // will depend wether readOnly is supported
|
|
2269
|
+
|
|
2270
|
+
"loading",
|
|
2271
|
+
"basePseudoState",
|
|
2272
|
+
"constraints",
|
|
2273
|
+
|
|
2274
|
+
// A real target inside a zone that belongs to another control — see
|
|
2275
|
+
// own_target.js.
|
|
2276
|
+
"ownTarget",
|
|
2277
|
+
|
|
2278
|
+
"autoFocus",
|
|
2279
|
+
"autoFocusVisible",
|
|
2280
|
+
"autoFocusSelect",
|
|
2281
|
+
|
|
2282
|
+
"onMouseDown",
|
|
2283
|
+
"onClick",
|
|
2284
|
+
"onKeyDown",
|
|
2285
|
+
"onPaste",
|
|
2286
|
+
"onInput",
|
|
2287
|
+
"eventReactionDefinitions",
|
|
2288
|
+
|
|
2289
|
+
"onCancel",
|
|
2290
|
+
"cancelOnBlurInvalid",
|
|
2291
|
+
"cancelOnEscape",
|
|
2292
|
+
"onActionPrevented",
|
|
2293
|
+
"onActionStart",
|
|
2294
|
+
"onActionAborted",
|
|
2295
|
+
"onActionError",
|
|
2296
|
+
"actionErrorEffect",
|
|
2297
|
+
"errorMapping",
|
|
2298
|
+
"onActionEnd",
|
|
2299
|
+
|
|
2300
|
+
"resetOnCancel",
|
|
2301
|
+
"resetOnAbort",
|
|
2302
|
+
"resetOnError",
|
|
2303
|
+
"optimistic",
|
|
2304
|
+
|
|
2305
|
+
"charGuard",
|
|
2306
|
+
"maxLengthGuard",
|
|
2307
|
+
]);
|
|
2308
|
+
|
|
2309
|
+
const MessagePropsRefContext = createContext();
|
|
2310
|
+
|
|
2311
|
+
const ControlIdContext = createContext();
|
|
2312
|
+
const ControlNameContext = createContext();
|
|
2313
|
+
const DisabledContext = createContext();
|
|
2314
|
+
const ReadOnlyContext = createContext();
|
|
2315
|
+
const RequiredContext = createContext();
|
|
2316
|
+
const LoadingContext$1 = createContext();
|
|
2317
|
+
createContext();
|
|
2318
|
+
|
|
2319
|
+
const ActionContext = createContext();
|
|
2320
|
+
const ActionRequesterContext = createContext();
|
|
2321
|
+
|
|
2322
|
+
/**
|
|
2323
|
+
* A target of its own inside a zone that belongs to another control.
|
|
2324
|
+
*
|
|
2325
|
+
* A pressable row, a picker's façade, a slide that travels under the finger:
|
|
2326
|
+
* each of them answers a press that lands anywhere in its box. An affordance an
|
|
2327
|
+
* application draws in there — a chip's cross, an eye that opens a profile, a
|
|
2328
|
+
* diskette that saves a guest — is aimed AT, not merely inside, and the press
|
|
2329
|
+
* belongs to it alone.
|
|
2330
|
+
*
|
|
2331
|
+
* Saying that by hand takes three guards, one per moment of the same press: the
|
|
2332
|
+
* pointerdown where gestures are arbitrated, the mousedown where a picker opens,
|
|
2333
|
+
* the click where it opens too when a gesture disputed the press. All three are
|
|
2334
|
+
* navi's own knowledge of navi's own event flow, and an application that gets
|
|
2335
|
+
* one wrong finds out by opening a popup it meant to keep shut. `ownTarget` is
|
|
2336
|
+
* that knowledge, said once.
|
|
2337
|
+
*
|
|
2338
|
+
* The other half is interactivity: the affordance is a control, so it already
|
|
2339
|
+
* refuses on its own terms once the zone around it holds it read-only — but a
|
|
2340
|
+
* caller's `onClick` is plain DOM and fires before any gate. So an own target
|
|
2341
|
+
* withholds the caller's handler until its own gate has allowed it, and by
|
|
2342
|
+
* default goes rather than greys: a remove cross that still removes is worse
|
|
2343
|
+
* than no cross, and one that refuses politely still says "there is something to
|
|
2344
|
+
* remove here" on a row that is only being read.
|
|
2345
|
+
*/
|
|
2346
|
+
|
|
2347
|
+
|
|
2348
|
+
const OWN_TARGET_ATTRIBUTE = "data-navi-own-target";
|
|
2349
|
+
|
|
2350
|
+
/**
|
|
2351
|
+
* Whether `event` was aimed at an own target sitting below this control — in
|
|
2352
|
+
* which case the control is not what the press was for and must not answer it.
|
|
2353
|
+
*
|
|
2354
|
+
* Read from the event's target rather than from a mark left by a handler: the
|
|
2355
|
+
* question is "who is this press for", and the DOM between the pointer and the
|
|
2356
|
+
* control is the whole answer. Nothing is asked of the own target itself, which
|
|
2357
|
+
* is what lets it be anything — a button, a link, a field.
|
|
2358
|
+
*/
|
|
2359
|
+
const isAimedAtOwnTargetBelow = (event, controlHost) => {
|
|
2360
|
+
const target = event?.target;
|
|
2361
|
+
if (!target || typeof target.closest !== "function") {
|
|
2362
|
+
return false;
|
|
2363
|
+
}
|
|
2364
|
+
const ownTarget = target.closest(`[${OWN_TARGET_ATTRIBUTE}]`);
|
|
2365
|
+
if (!ownTarget) {
|
|
2366
|
+
return false;
|
|
2367
|
+
}
|
|
2368
|
+
// The claim is against what is ABOVE it: the control that IS the own target,
|
|
2369
|
+
// and any control living inside it, are being aimed at like anything else.
|
|
2370
|
+
if (ownTarget === controlHost || ownTarget.contains(controlHost)) {
|
|
2371
|
+
return false;
|
|
2372
|
+
}
|
|
2373
|
+
// From the root rather than the host: a layered control (a picker holding an
|
|
2374
|
+
// input) has its gate on the input, and what the application drew sits beside
|
|
2375
|
+
// it, not in it.
|
|
2376
|
+
const controlRoot = controlHost.closest("[navi-control]") || controlHost;
|
|
2377
|
+
return controlRoot.contains(ownTarget);
|
|
2378
|
+
};
|
|
2379
|
+
|
|
2380
|
+
/**
|
|
2381
|
+
* Whether an own target has nothing to offer where it sits: the zone around it
|
|
2382
|
+
* is read-only, disabled or busy, so the affordance goes.
|
|
2383
|
+
*
|
|
2384
|
+
* `ownTarget="refuse"` keeps it on screen instead, refusing with a callout like
|
|
2385
|
+
* every other navi control — for an affordance whose presence is information in
|
|
2386
|
+
* itself (an eye that opens a profile is worth seeing on a row being read).
|
|
2387
|
+
*/
|
|
2388
|
+
const useOwnTargetHidden = (props) => {
|
|
2389
|
+
const disabled = useContext(DisabledContext);
|
|
2390
|
+
const readOnly = useContext(ReadOnlyContext);
|
|
2391
|
+
const loading = useContext(LoadingContext$1);
|
|
2392
|
+
const { ownTarget } = props;
|
|
2393
|
+
if (!ownTarget || ownTarget === "refuse") {
|
|
2394
|
+
return false;
|
|
2395
|
+
}
|
|
2396
|
+
return Boolean(disabled || readOnly || loading);
|
|
2397
|
+
};
|
|
2398
|
+
|
|
2117
2399
|
/*
|
|
2118
2400
|
* Deep structural equality for arbitrary JS values — what `===` can't do but this
|
|
2119
2401
|
* codebase constantly needs: memoization cache keys ({ id: 1 } equal to { id: 1 }),
|
|
@@ -6490,83 +6772,6 @@ const isInertOnClick = (element) => {
|
|
|
6490
6772
|
return true;
|
|
6491
6773
|
};
|
|
6492
6774
|
|
|
6493
|
-
const CONSTRAINT_NAME_TO_PROP = {
|
|
6494
|
-
disabled: "disabledMessage",
|
|
6495
|
-
required: "requiredMessage",
|
|
6496
|
-
pattern: "patternMessage",
|
|
6497
|
-
type_email: "typeMessage",
|
|
6498
|
-
type_number: "typeMessage",
|
|
6499
|
-
min_length: "minLengthMessage",
|
|
6500
|
-
max_length: "maxLengthMessage",
|
|
6501
|
-
min: "minMessage",
|
|
6502
|
-
max: "maxMessage",
|
|
6503
|
-
single_space: "singleSpaceMessage",
|
|
6504
|
-
same_as: "sameAsMessage",
|
|
6505
|
-
min_lower_letter: "minLowerLetterMessage",
|
|
6506
|
-
min_upper_letter: "minUpperLetterMessage",
|
|
6507
|
-
min_digit: "minDigitMessage",
|
|
6508
|
-
min_special_char: "minSpecialCharMessage",
|
|
6509
|
-
one_of: "oneOfMessage",
|
|
6510
|
-
readonly: "readOnlyMessage",
|
|
6511
|
-
available: "availableMessage",
|
|
6512
|
-
};
|
|
6513
|
-
|
|
6514
|
-
const CONSTRAINT_MESSAGE_PROP_NAME_SET = new Set(
|
|
6515
|
-
Object.values(CONSTRAINT_NAME_TO_PROP),
|
|
6516
|
-
);
|
|
6517
|
-
|
|
6518
|
-
const extractMessageAndRemainingProps = (props) => {
|
|
6519
|
-
const ownMessages = {};
|
|
6520
|
-
const remaining = {};
|
|
6521
|
-
const keyToVisit = new Set(Object.keys(props));
|
|
6522
|
-
for (const key of keyToVisit) {
|
|
6523
|
-
if (CONSTRAINT_MESSAGE_PROP_NAME_SET.has(key)) {
|
|
6524
|
-
ownMessages[key] = props[key];
|
|
6525
|
-
} else {
|
|
6526
|
-
remaining[key] = props[key];
|
|
6527
|
-
}
|
|
6528
|
-
}
|
|
6529
|
-
return [ownMessages, remaining];
|
|
6530
|
-
};
|
|
6531
|
-
|
|
6532
|
-
const getConstraintMessage = (
|
|
6533
|
-
controller,
|
|
6534
|
-
constraint,
|
|
6535
|
-
generatedMessage,
|
|
6536
|
-
{ requester },
|
|
6537
|
-
) => {
|
|
6538
|
-
const { name: constraintName } = constraint;
|
|
6539
|
-
const propName = CONSTRAINT_NAME_TO_PROP[constraintName];
|
|
6540
|
-
|
|
6541
|
-
// 1. Search first on the requester (e.g. the <li> that was clicked),
|
|
6542
|
-
// then fall back to element (e.g. the hidden <input>).
|
|
6543
|
-
if (requester) {
|
|
6544
|
-
const requesterController = requester.__uiStateController__;
|
|
6545
|
-
if (requesterController && requesterController !== controller) {
|
|
6546
|
-
const requesterControllerMessage = requesterController.props[propName];
|
|
6547
|
-
if (requesterControllerMessage) {
|
|
6548
|
-
return {
|
|
6549
|
-
message: requesterControllerMessage,
|
|
6550
|
-
origin: "requester controller",
|
|
6551
|
-
};
|
|
6552
|
-
}
|
|
6553
|
-
}
|
|
6554
|
-
}
|
|
6555
|
-
|
|
6556
|
-
const controllerMessage = controller.props[propName];
|
|
6557
|
-
if (controllerMessage) {
|
|
6558
|
-
return {
|
|
6559
|
-
message: controllerMessage,
|
|
6560
|
-
origin: "controller",
|
|
6561
|
-
};
|
|
6562
|
-
}
|
|
6563
|
-
|
|
6564
|
-
return {
|
|
6565
|
-
message: generatedMessage,
|
|
6566
|
-
origin: "generated message",
|
|
6567
|
-
};
|
|
6568
|
-
};
|
|
6569
|
-
|
|
6570
6775
|
/**
|
|
6571
6776
|
* DOM utilities for the proxy control pattern.
|
|
6572
6777
|
*
|
|
@@ -8241,8 +8446,6 @@ const ABORTED = { id: "aborted" };
|
|
|
8241
8446
|
const FAILED = { id: "failed" };
|
|
8242
8447
|
const COMPLETED = { id: "completed" };
|
|
8243
8448
|
|
|
8244
|
-
const CONSTRAINT_ATTRIBUTE_SET = new Set();
|
|
8245
|
-
|
|
8246
8449
|
const BUSY_CONSTRAINT = {
|
|
8247
8450
|
name: "busy",
|
|
8248
8451
|
messageAttribute: "data-busy-message",
|
|
@@ -10295,6 +10498,20 @@ const onRequestInteraction = (
|
|
|
10295
10498
|
|
|
10296
10499
|
const currentTarget = requestInteractionCustomEvent.currentTarget;
|
|
10297
10500
|
const controlHost = findControlHost(currentTarget) || currentTarget;
|
|
10501
|
+
|
|
10502
|
+
// Aimed at something else that lives in this control's box: a chip's cross, an
|
|
10503
|
+
// eye, a diskette. The press is that affordance's alone (see own_target.js),
|
|
10504
|
+
// and stepping back here — rather than stopping the propagation over there —
|
|
10505
|
+
// is what leaves the event whole for everything that is not a navi
|
|
10506
|
+
// interaction. Stepping back, not refusing: the reaction never happened, so
|
|
10507
|
+
// its `prevented`/`always` (an `e.preventDefault()`, for most of them) have
|
|
10508
|
+
// nothing to undo and would take the press from the affordance itself.
|
|
10509
|
+
if (isAimedAtOwnTargetBelow(event, controlHost)) {
|
|
10510
|
+
debugInteraction(event, `"${name}" is for an own target below`);
|
|
10511
|
+
requestInteractionCustomEvent.preventDefault();
|
|
10512
|
+
return false;
|
|
10513
|
+
}
|
|
10514
|
+
|
|
10298
10515
|
const controller = controlHost.__uiStateController__;
|
|
10299
10516
|
|
|
10300
10517
|
if (controller && !bypassInteractivity) {
|
|
@@ -16085,6 +16302,26 @@ const POSITION_PROPS = {
|
|
|
16085
16302
|
return { transform: `skew(${value})` };
|
|
16086
16303
|
},
|
|
16087
16304
|
};
|
|
16305
|
+
const singleLineEllipsisStyles = () => {
|
|
16306
|
+
return {
|
|
16307
|
+
overflow: "hidden",
|
|
16308
|
+
textOverflow: "ellipsis",
|
|
16309
|
+
overflowWrap: "normal",
|
|
16310
|
+
};
|
|
16311
|
+
};
|
|
16312
|
+
// The lines beyond the clamp are still laid out, so the clip edge decides
|
|
16313
|
+
// whether the top of the first hidden one is visible: "overflow: hidden" clips
|
|
16314
|
+
// at the padding box and lets it show inside the block-end padding. Clipping at
|
|
16315
|
+
// the content box instead ends the element right after its last visible line.
|
|
16316
|
+
const lineClampStyles = (value) => {
|
|
16317
|
+
return {
|
|
16318
|
+
"overflow": "clip",
|
|
16319
|
+
"overflowClipMargin": "content-box",
|
|
16320
|
+
"display": "-webkit-box",
|
|
16321
|
+
"-webkit-box-orient": "vertical",
|
|
16322
|
+
"-webkit-line-clamp": value,
|
|
16323
|
+
};
|
|
16324
|
+
};
|
|
16088
16325
|
const TYPO_PROPS = {
|
|
16089
16326
|
font: applyOnCSSProp("fontFamily"),
|
|
16090
16327
|
fontFamily: PASS_THROUGH,
|
|
@@ -16122,39 +16359,21 @@ const TYPO_PROPS = {
|
|
|
16122
16359
|
return null;
|
|
16123
16360
|
}
|
|
16124
16361
|
if (value === 1 || value === "1") {
|
|
16125
|
-
return
|
|
16126
|
-
overflow: "hidden",
|
|
16127
|
-
textOverflow: "ellipsis",
|
|
16128
|
-
overflowWrap: "normal",
|
|
16129
|
-
};
|
|
16362
|
+
return singleLineEllipsisStyles();
|
|
16130
16363
|
}
|
|
16131
|
-
return
|
|
16132
|
-
"overflow": "hidden",
|
|
16133
|
-
"display": "-webkit-box",
|
|
16134
|
-
"-webkit-box-orient": "vertical",
|
|
16135
|
-
"-webkit-line-clamp": value,
|
|
16136
|
-
};
|
|
16364
|
+
return lineClampStyles(value);
|
|
16137
16365
|
},
|
|
16138
16366
|
overflowEllipsis: (value) => {
|
|
16139
16367
|
if (!value) {
|
|
16140
16368
|
return null;
|
|
16141
16369
|
}
|
|
16142
|
-
return
|
|
16143
|
-
overflow: "hidden",
|
|
16144
|
-
textOverflow: "ellipsis",
|
|
16145
|
-
overflowWrap: "normal",
|
|
16146
|
-
};
|
|
16370
|
+
return singleLineEllipsisStyles();
|
|
16147
16371
|
},
|
|
16148
16372
|
lineClamp: (value) => {
|
|
16149
16373
|
if (!value) {
|
|
16150
16374
|
return null;
|
|
16151
16375
|
}
|
|
16152
|
-
return
|
|
16153
|
-
"overflow": "hidden",
|
|
16154
|
-
"display": "-webkit-box",
|
|
16155
|
-
"-webkit-box-orient": "vertical",
|
|
16156
|
-
"-webkit-line-clamp": value,
|
|
16157
|
-
};
|
|
16376
|
+
return lineClampStyles(value);
|
|
16158
16377
|
},
|
|
16159
16378
|
textAlign: PASS_THROUGH,
|
|
16160
16379
|
textBox: PASS_THROUGH,
|
|
@@ -16200,6 +16419,7 @@ const VISUAL_PROPS = {
|
|
|
16200
16419
|
overflow: PASS_THROUGH,
|
|
16201
16420
|
overflowX: PASS_THROUGH,
|
|
16202
16421
|
overflowY: PASS_THROUGH,
|
|
16422
|
+
overflowClipMargin: PASS_THROUGH,
|
|
16203
16423
|
objectFit: PASS_THROUGH,
|
|
16204
16424
|
accentColor: PASS_THROUGH,
|
|
16205
16425
|
scrollbarWidth: PASS_THROUGH,
|
|
@@ -23467,116 +23687,6 @@ registerNaviCommand("--navi-unselect", (source, event) => {
|
|
|
23467
23687
|
};
|
|
23468
23688
|
});
|
|
23469
23689
|
|
|
23470
|
-
// prop that we'll set on the control
|
|
23471
|
-
const CONTROL_ATTRIBUTE_SET = new Set([
|
|
23472
|
-
...CONSTRAINT_ATTRIBUTE_SET,
|
|
23473
|
-
|
|
23474
|
-
"ref",
|
|
23475
|
-
"children",
|
|
23476
|
-
"id",
|
|
23477
|
-
"name",
|
|
23478
|
-
"type",
|
|
23479
|
-
"value",
|
|
23480
|
-
"checked",
|
|
23481
|
-
"placeholder",
|
|
23482
|
-
"inputMode",
|
|
23483
|
-
"autoComplete",
|
|
23484
|
-
"spellcheck",
|
|
23485
|
-
"autoCorrect",
|
|
23486
|
-
"aria-controls",
|
|
23487
|
-
"tabIndex",
|
|
23488
|
-
"command",
|
|
23489
|
-
"commandFor",
|
|
23490
|
-
"command-value", // not standard but make sense, allow to give param to the command in question
|
|
23491
|
-
"list",
|
|
23492
|
-
|
|
23493
|
-
// "ui-action-target",
|
|
23494
|
-
"navi-input-type",
|
|
23495
|
-
"navi-value-pad",
|
|
23496
|
-
"navi-control-proxy-for",
|
|
23497
|
-
"navi-command-proxy-for",
|
|
23498
|
-
"navi-command-target",
|
|
23499
|
-
"onnavi_command",
|
|
23500
|
-
"onnavi_request_open",
|
|
23501
|
-
"onnavi_request_close",
|
|
23502
|
-
|
|
23503
|
-
"data-callout-arrow-x",
|
|
23504
|
-
"data-callout-point-to-border-box",
|
|
23505
|
-
"data-callout-point-to-content-box",
|
|
23506
|
-
"data-callout-viewport-spacing",
|
|
23507
|
-
"data-callout-position",
|
|
23508
|
-
"data-callout-position-fixed",
|
|
23509
|
-
|
|
23510
|
-
"data-testid", // playwright, cypress
|
|
23511
|
-
"data-separator", // used by InputGroup paste-to-fill
|
|
23512
|
-
]);
|
|
23513
|
-
// prop concerning control but that won't end up in the DOM if not inside CONTROL_ATTRIBUTE_SET
|
|
23514
|
-
const CONTROL_PROP_SET = new Set([
|
|
23515
|
-
...CONTROL_ATTRIBUTE_SET,
|
|
23516
|
-
...CONSTRAINT_MESSAGE_PROP_NAME_SET,
|
|
23517
|
-
|
|
23518
|
-
"action",
|
|
23519
|
-
"confirm",
|
|
23520
|
-
"confirmPopupContent",
|
|
23521
|
-
"actionEvent",
|
|
23522
|
-
"actionAfterChange",
|
|
23523
|
-
"actionOnMouseDown",
|
|
23524
|
-
"actionDebounce",
|
|
23525
|
-
// A signal bound two-way to the control: its value seeds the control's state and
|
|
23526
|
-
// is written back on every uiAction. Precludes value/checked (see createControlInfo).
|
|
23527
|
-
"signal",
|
|
23528
|
-
"defaultValue",
|
|
23529
|
-
"defaultChecked",
|
|
23530
|
-
"readOnly", // will depend wether readOnly is supported
|
|
23531
|
-
|
|
23532
|
-
"loading",
|
|
23533
|
-
"basePseudoState",
|
|
23534
|
-
"constraints",
|
|
23535
|
-
|
|
23536
|
-
"autoFocus",
|
|
23537
|
-
"autoFocusVisible",
|
|
23538
|
-
"autoFocusSelect",
|
|
23539
|
-
|
|
23540
|
-
"onMouseDown",
|
|
23541
|
-
"onClick",
|
|
23542
|
-
"onKeyDown",
|
|
23543
|
-
"onPaste",
|
|
23544
|
-
"onInput",
|
|
23545
|
-
"eventReactionDefinitions",
|
|
23546
|
-
|
|
23547
|
-
"onCancel",
|
|
23548
|
-
"cancelOnBlurInvalid",
|
|
23549
|
-
"cancelOnEscape",
|
|
23550
|
-
"onActionPrevented",
|
|
23551
|
-
"onActionStart",
|
|
23552
|
-
"onActionAborted",
|
|
23553
|
-
"onActionError",
|
|
23554
|
-
"actionErrorEffect",
|
|
23555
|
-
"errorMapping",
|
|
23556
|
-
"onActionEnd",
|
|
23557
|
-
|
|
23558
|
-
"resetOnCancel",
|
|
23559
|
-
"resetOnAbort",
|
|
23560
|
-
"resetOnError",
|
|
23561
|
-
"optimistic",
|
|
23562
|
-
|
|
23563
|
-
"charGuard",
|
|
23564
|
-
"maxLengthGuard",
|
|
23565
|
-
]);
|
|
23566
|
-
|
|
23567
|
-
const MessagePropsRefContext = createContext();
|
|
23568
|
-
|
|
23569
|
-
const ControlIdContext = createContext();
|
|
23570
|
-
const ControlNameContext = createContext();
|
|
23571
|
-
const DisabledContext = createContext();
|
|
23572
|
-
const ReadOnlyContext = createContext();
|
|
23573
|
-
const RequiredContext = createContext();
|
|
23574
|
-
const LoadingContext$1 = createContext();
|
|
23575
|
-
createContext();
|
|
23576
|
-
|
|
23577
|
-
const ActionContext = createContext();
|
|
23578
|
-
const ActionRequesterContext = createContext();
|
|
23579
|
-
|
|
23580
23690
|
/**
|
|
23581
23691
|
* How a control tells the labels pointing at it what it is (disabled, readOnly,
|
|
23582
23692
|
* required) and when it goes away.
|
|
@@ -25798,18 +25908,18 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
25798
25908
|
if (child !== firstChildControllerRef.current) {
|
|
25799
25909
|
return;
|
|
25800
25910
|
}
|
|
25801
|
-
if (
|
|
25802
|
-
silent &&
|
|
25803
|
-
uiStateHoldsNothing(child.uiState) &&
|
|
25804
|
-
!uiStateHoldsNothing(s.realUIStateController.uiState)
|
|
25805
|
-
) {
|
|
25911
|
+
if (silent && uiStateHoldsNothing(child.uiState)) {
|
|
25806
25912
|
// A silent sync means the child's own structure changed (children
|
|
25807
25913
|
// mounted/unmounted), not that the user acted. A child that ends up
|
|
25808
25914
|
// with no value there is one that currently *cannot* express one —
|
|
25809
25915
|
// a <List loading> holds no items yet, a popup whose items are gone
|
|
25810
25916
|
// aggregates to the empty array its stateType falls back to — which
|
|
25811
25917
|
// must not read as the user clearing the picker, nor fire its
|
|
25812
|
-
// uiAction.
|
|
25918
|
+
// uiAction. And when the picker holds nothing either, there is
|
|
25919
|
+
// still nothing to adopt: the sync would only respell one nothing
|
|
25920
|
+
// as another (an array picker's [] becoming undefined, say) and
|
|
25921
|
+
// hand that to uiAction — an empty array picker opened its popup
|
|
25922
|
+
// and told its owner the value changed.
|
|
25813
25923
|
return;
|
|
25814
25924
|
}
|
|
25815
25925
|
updatingRef.current = true;
|
|
@@ -25822,7 +25932,18 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
25822
25932
|
detail: {},
|
|
25823
25933
|
});
|
|
25824
25934
|
chainEvent(propagateUpEvent, e);
|
|
25825
|
-
s
|
|
25935
|
+
// What the popup aggregates arrives in the popup's terms, where an
|
|
25936
|
+
// empty multiple list is `undefined`. The picker answers a question of
|
|
25937
|
+
// its own shape (see resolveEmptyUIState): a <Picker type="array">
|
|
25938
|
+
// whose last item was unselected holds [], the same thing clearing it
|
|
25939
|
+
// leaves — not a value that changes type on its owner the moment it
|
|
25940
|
+
// empties.
|
|
25941
|
+
const { emptyUIState } = s.realUIStateController;
|
|
25942
|
+
const uiStateToAdopt =
|
|
25943
|
+
child.uiState === undefined && emptyUIState !== undefined
|
|
25944
|
+
? emptyUIState
|
|
25945
|
+
: child.uiState;
|
|
25946
|
+
s.realUIStateController.setUIState(uiStateToAdopt, propagateUpEvent);
|
|
25826
25947
|
updatingRef.current = false;
|
|
25827
25948
|
},
|
|
25828
25949
|
};
|
|
@@ -26732,11 +26853,29 @@ const useControlProps = (props, {
|
|
|
26732
26853
|
}
|
|
26733
26854
|
return dispatched;
|
|
26734
26855
|
};
|
|
26735
|
-
|
|
26856
|
+
|
|
26857
|
+
// What the caller wrote runs from inside the gate when this control is an
|
|
26858
|
+
// own target: a plain `onClick` is DOM, and it would fire from a cross drawn
|
|
26859
|
+
// greyed by the read-only control the affordance sits in (see own_target.js).
|
|
26860
|
+
const callerHandlerIsGated = Boolean(props.ownTarget);
|
|
26861
|
+
const gateCallerHandler = (handler, e) => {
|
|
26862
|
+
if (!handler) {
|
|
26863
|
+
return undefined;
|
|
26864
|
+
}
|
|
26865
|
+
if (callerHandlerIsGated) {
|
|
26866
|
+
return handler;
|
|
26867
|
+
}
|
|
26868
|
+
handler(e);
|
|
26869
|
+
return undefined;
|
|
26870
|
+
};
|
|
26871
|
+
const applyEventReaction = (eventName, e, callerHandler) => {
|
|
26736
26872
|
const defaultEventReactionDefinition = defaultEventReactionDefinitions?.[eventName];
|
|
26737
26873
|
const customEventReactionDefinition = eventReactionDefinitions?.[eventName];
|
|
26738
26874
|
const reaction = customEventReactionDefinition?.(e) ?? defaultEventReactionDefinition?.(e);
|
|
26739
26875
|
if (!reaction) {
|
|
26876
|
+
// No reaction means no gate: there is nothing here that could refuse the
|
|
26877
|
+
// caller's handler, so withholding it would only lose it.
|
|
26878
|
+
callerHandler?.(e);
|
|
26740
26879
|
return false;
|
|
26741
26880
|
}
|
|
26742
26881
|
const {
|
|
@@ -26761,19 +26900,18 @@ const useControlProps = (props, {
|
|
|
26761
26900
|
prevented?.();
|
|
26762
26901
|
},
|
|
26763
26902
|
allowed: () => {
|
|
26903
|
+
callerHandler?.(e);
|
|
26764
26904
|
allowed?.();
|
|
26765
26905
|
},
|
|
26766
26906
|
always
|
|
26767
26907
|
});
|
|
26768
26908
|
};
|
|
26769
26909
|
const onMouseDown = e => {
|
|
26770
|
-
props.onMouseDown
|
|
26771
|
-
applyEventReaction("mouseDown", e);
|
|
26910
|
+
applyEventReaction("mouseDown", e, gateCallerHandler(props.onMouseDown, e));
|
|
26772
26911
|
transferFocusToTarget(e);
|
|
26773
26912
|
};
|
|
26774
26913
|
const onClick = e => {
|
|
26775
|
-
props.onClick
|
|
26776
|
-
applyEventReaction("click", e);
|
|
26914
|
+
applyEventReaction("click", e, gateCallerHandler(props.onClick, e));
|
|
26777
26915
|
transferFocusToTarget(e);
|
|
26778
26916
|
};
|
|
26779
26917
|
const onKeyDown = e => {
|
|
@@ -27296,6 +27434,15 @@ const useInteractiveProps = (props, {
|
|
|
27296
27434
|
} = props;
|
|
27297
27435
|
const [controlRootProps, controlHostProps] = splitControlProps(props);
|
|
27298
27436
|
controlRootProps["navi-control"] = controlInfo.controlType;
|
|
27437
|
+
if (props.ownTarget) {
|
|
27438
|
+
// "This press is mine" said in the DOM, because that is where it is read
|
|
27439
|
+
// from the outside: by the controls above (see own_target.js), and by the
|
|
27440
|
+
// two gesture readers below — the one that travels a box and the one that
|
|
27441
|
+
// carries a piece, each with its own way of being told to keep out.
|
|
27442
|
+
controlRootProps[OWN_TARGET_ATTRIBUTE] = "";
|
|
27443
|
+
controlRootProps["data-no-drag-travel"] = "";
|
|
27444
|
+
controlRootProps["data-drag-ignore"] = "";
|
|
27445
|
+
}
|
|
27299
27446
|
const {
|
|
27300
27447
|
"navi-control-proxy-for": naviProxyFor
|
|
27301
27448
|
} = props;
|
|
@@ -28412,6 +28559,7 @@ const ButtonFirstResolver = props => {
|
|
|
28412
28559
|
const Next = useNextResolver();
|
|
28413
28560
|
const defaultRef = useRef(null);
|
|
28414
28561
|
props.ref = props.ref || defaultRef;
|
|
28562
|
+
const ownTargetHidden = useOwnTargetHidden(props);
|
|
28415
28563
|
|
|
28416
28564
|
// Attached to the element rather than kept as a prop: the action a button
|
|
28417
28565
|
// requests is not always run by the button (a submit button hands the send to
|
|
@@ -28421,6 +28569,9 @@ const ButtonFirstResolver = props => {
|
|
|
28421
28569
|
message: props.confirm,
|
|
28422
28570
|
content: props.confirmPopupContent
|
|
28423
28571
|
});
|
|
28572
|
+
if (ownTargetHidden) {
|
|
28573
|
+
return null;
|
|
28574
|
+
}
|
|
28424
28575
|
return jsx(Next, {
|
|
28425
28576
|
...props
|
|
28426
28577
|
});
|
|
@@ -28511,6 +28662,21 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
28511
28662
|
children: naviI18n("button.open")
|
|
28512
28663
|
})
|
|
28513
28664
|
};
|
|
28665
|
+
|
|
28666
|
+
/**
|
|
28667
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
28668
|
+
* ownTarget?: boolean | "refuse",
|
|
28669
|
+
* [key: string]: any,
|
|
28670
|
+
* }>}
|
|
28671
|
+
* @param {boolean|"refuse"} [ownTarget] A real target inside a zone that belongs
|
|
28672
|
+
* to another control — a chip's cross on a picker's façade, an eye on a
|
|
28673
|
+
* pressable row, a diskette inside a slide that travels. The press is this
|
|
28674
|
+
* button's alone (no travel starts, no popup opens, nothing above answers) and
|
|
28675
|
+
* its `onClick` waits for its own interaction gate instead of firing from the
|
|
28676
|
+
* DOM. Where the zone around it is read-only, disabled or busy the button
|
|
28677
|
+
* goes; `"refuse"` keeps it on screen refusing with a callout, for an
|
|
28678
|
+
* affordance whose presence is information in itself.
|
|
28679
|
+
*/
|
|
28514
28680
|
const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
|
|
28515
28681
|
|
|
28516
28682
|
// How long a popup waits before handing the focus to a field, when giving it
|
|
@@ -30071,6 +30237,10 @@ const css$_ = /* css */`
|
|
|
30071
30237
|
padding: 0;
|
|
30072
30238
|
flex-direction: column;
|
|
30073
30239
|
|
|
30240
|
+
/* A new surface writes in its own ink, not in its container's — see
|
|
30241
|
+
--navi-popup-color. Declared here rather than left to the UA's own
|
|
30242
|
+
dialog { color: CanvasText } so the ink is themed along with the paper. */
|
|
30243
|
+
color: var(--navi-popup-color);
|
|
30074
30244
|
background-color: var(--dialog-background-color);
|
|
30075
30245
|
border-width: var(--dialog-border-width);
|
|
30076
30246
|
border-style: solid;
|
|
@@ -31600,6 +31770,10 @@ const css$Z = /* css */`
|
|
|
31600
31770
|
var(--x-popover-max-height)
|
|
31601
31771
|
);
|
|
31602
31772
|
max-height: var(--x-popover-max-height);
|
|
31773
|
+
/* A new surface writes in its own ink, not in its container's — see
|
|
31774
|
+
--navi-popup-color. The UA only resets color on a [popover] element,
|
|
31775
|
+
and the local renderer is a plain div. */
|
|
31776
|
+
color: var(--navi-popup-color);
|
|
31603
31777
|
background-color: var(--popover-background-color);
|
|
31604
31778
|
border-width: var(--popover-border-width);
|
|
31605
31779
|
border-style: solid;
|
|
@@ -55463,6 +55637,16 @@ const isTextInputElement = (el) => {
|
|
|
55463
55637
|
);
|
|
55464
55638
|
};
|
|
55465
55639
|
|
|
55640
|
+
// How many lines the thing around a component gives it, when that thing caps
|
|
55641
|
+
// its own height and cannot cap what it holds.
|
|
55642
|
+
//
|
|
55643
|
+
// A <Picker> is the case it exists for: its value is clamped with maxLines,
|
|
55644
|
+
// which is CSS line-clamp — it counts line boxes of inline text and has no idea
|
|
55645
|
+
// what a wrapped flex row is. So a <BadgeList> rendered as a picker's ui reads
|
|
55646
|
+
// the number from here and caps its own rows to it, and the picker turns its
|
|
55647
|
+
// own clamp off (see .navi_picker_value:has(.navi_badge_list) in picker.jsx).
|
|
55648
|
+
const MaxLinesContext = createContext(undefined);
|
|
55649
|
+
|
|
55466
55650
|
// When a component render a prop that can be anything (js value of preact element)
|
|
55467
55651
|
// make sure it cannot throw during render by converting it to a string if it's not a valid preact element or a primitive value
|
|
55468
55652
|
const renderSafe = (value) => {
|
|
@@ -57861,12 +58045,24 @@ const ListItemSelectable = props => {
|
|
|
57861
58045
|
...rest
|
|
57862
58046
|
} = props;
|
|
57863
58047
|
const multiple = useContext(SelectableListMultipleContext);
|
|
58048
|
+
// Whose reason it is that this row cannot be taken. Read-only reaching it
|
|
58049
|
+
// from above is the LIST's, and what is settled is then the whole answer —
|
|
58050
|
+
// said as the selection where several things are taken, as the choice where
|
|
58051
|
+
// one thing is. Said of each row in turn, "this option is not available"
|
|
58052
|
+
// describes something else entirely: a list where each row happens to be
|
|
58053
|
+
// unavailable for its own reasons, which goes on being said that way. Busy
|
|
58054
|
+
// is not settled either: a list waiting on something says nothing about what
|
|
58055
|
+
// will be possible once it is done, so the row keeps its own words.
|
|
58056
|
+
const readOnlyFromAbove = useContext(ReadOnlyContext);
|
|
58057
|
+
const loadingFromAbove = useContext(LoadingContext$1);
|
|
58058
|
+
const answerIsSettled = Boolean(readOnlyFromAbove) && !loadingFromAbove;
|
|
58059
|
+
const readOnlyMessageKey = answerIsSettled ? multiple ? `constraint.readonly.selection` : `constraint.readonly.choice` : `constraint.readonly.option`;
|
|
57864
58060
|
const inputRef = useRef();
|
|
57865
58061
|
const inputType = multiple ? "checkbox" : "radio";
|
|
57866
58062
|
const inputId = `${id}_input`;
|
|
57867
58063
|
inputRef.nullCanHappen = true; // virtualization
|
|
57868
58064
|
const [checkableRootProps, checkableProps, controlChildrenWrapperProps] = useCheckableProps({
|
|
57869
|
-
readOnlyMessage: naviI18n(
|
|
58065
|
+
readOnlyMessage: naviI18n(readOnlyMessageKey, props),
|
|
57870
58066
|
...rest,
|
|
57871
58067
|
ref: inputRef,
|
|
57872
58068
|
id: inputId,
|
|
@@ -62511,6 +62707,51 @@ const PickerPresetResolver = props => {
|
|
|
62511
62707
|
});
|
|
62512
62708
|
};
|
|
62513
62709
|
|
|
62710
|
+
// Put around its children by BadgeList so a Badge below knows it is inside one.
|
|
62711
|
+
// A badge then renders nothing at all: it hands its props to the list and the
|
|
62712
|
+
// list renders it. Badges register in tree order, so by the time the list gets
|
|
62713
|
+
// to its own content it holds them all, in source order, and knows how many
|
|
62714
|
+
// there are before deciding what to show — without ever walking children
|
|
62715
|
+
// vnodes, and without rendering a badge it then has to take back.
|
|
62716
|
+
const BadgeListContext = createContext(null);
|
|
62717
|
+
|
|
62718
|
+
const createBadgeRegistry = () => {
|
|
62719
|
+
let pass = 0;
|
|
62720
|
+
let passCount = 0;
|
|
62721
|
+
let entries = [];
|
|
62722
|
+
let childrenAreNew = true;
|
|
62723
|
+
|
|
62724
|
+
return {
|
|
62725
|
+
// Called by BadgeList at the top of every render. childrenAreNew says
|
|
62726
|
+
// whether it was handed fresh children vnodes: when it re-renders on its
|
|
62727
|
+
// own (its own state changed) Preact hands the untouched children straight
|
|
62728
|
+
// back and skips them, so none of the badges registers again. That empty
|
|
62729
|
+
// pass means "unchanged", not "no badge left" — see getEntries.
|
|
62730
|
+
startPass: (areNew) => {
|
|
62731
|
+
pass++;
|
|
62732
|
+
passCount = 0;
|
|
62733
|
+
childrenAreNew = areNew;
|
|
62734
|
+
},
|
|
62735
|
+
// entryState is the badge's own memory. A badge that re-renders on its own
|
|
62736
|
+
// must update its entry, not append a second one.
|
|
62737
|
+
register: (entryState, props) => {
|
|
62738
|
+
if (entryState.pass !== pass) {
|
|
62739
|
+
entryState.pass = pass;
|
|
62740
|
+
entryState.index = passCount;
|
|
62741
|
+
passCount++;
|
|
62742
|
+
}
|
|
62743
|
+
entries[entryState.index] = props;
|
|
62744
|
+
},
|
|
62745
|
+
getEntries: () => {
|
|
62746
|
+
if (passCount === 0 && !childrenAreNew) {
|
|
62747
|
+
return entries;
|
|
62748
|
+
}
|
|
62749
|
+
entries.length = passCount;
|
|
62750
|
+
return entries;
|
|
62751
|
+
},
|
|
62752
|
+
};
|
|
62753
|
+
};
|
|
62754
|
+
|
|
62514
62755
|
installImportMetaCssBuild(import.meta);const css$x = /* css */`
|
|
62515
62756
|
@layer navi {
|
|
62516
62757
|
}
|
|
@@ -62569,6 +62810,11 @@ installImportMetaCssBuild(import.meta);const css$x = /* css */`
|
|
|
62569
62810
|
align-items: stretch;
|
|
62570
62811
|
color: var(--x-color);
|
|
62571
62812
|
font-size: var(--font-size);
|
|
62813
|
+
/* Cuts the font's half-leading above the first line and below the last one,
|
|
62814
|
+
down to cap-height/baseline: the padding becomes the only vertical space
|
|
62815
|
+
and the text is exactly centered. Space between wrapped lines is left
|
|
62816
|
+
untouched, unlike a line-height tweak. */
|
|
62817
|
+
text-box: trim-both cap alphabetic;
|
|
62572
62818
|
background: var(--x-background);
|
|
62573
62819
|
background-color: var(--x-background-color);
|
|
62574
62820
|
border-radius: 1em;
|
|
@@ -62618,7 +62864,22 @@ installImportMetaCssBuild(import.meta);const css$x = /* css */`
|
|
|
62618
62864
|
}
|
|
62619
62865
|
}
|
|
62620
62866
|
`;
|
|
62621
|
-
const Badge =
|
|
62867
|
+
const Badge = props => {
|
|
62868
|
+
const badgeList = useContext(BadgeListContext);
|
|
62869
|
+
const entryStateRef = useRef();
|
|
62870
|
+
if (badgeList) {
|
|
62871
|
+
// Inside a BadgeList the badge renders nothing: it hands itself over and
|
|
62872
|
+
// the list renders it, which is how the list gets to see them all before
|
|
62873
|
+
// deciding how many it shows. See badge_list_context.js.
|
|
62874
|
+
const entryState = entryStateRef.current || (entryStateRef.current = {});
|
|
62875
|
+
badgeList.register(entryState, props);
|
|
62876
|
+
return null;
|
|
62877
|
+
}
|
|
62878
|
+
return jsx(BadgeUI, {
|
|
62879
|
+
...props
|
|
62880
|
+
});
|
|
62881
|
+
};
|
|
62882
|
+
const BadgeUI = ({
|
|
62622
62883
|
children,
|
|
62623
62884
|
className,
|
|
62624
62885
|
...props
|
|
@@ -62633,7 +62894,14 @@ const Badge = ({
|
|
|
62633
62894
|
return jsx(Text, {
|
|
62634
62895
|
className: withPropsClassName("navi_badge", className),
|
|
62635
62896
|
bold: true,
|
|
62636
|
-
maxLines: 1
|
|
62897
|
+
maxLines: 1
|
|
62898
|
+
// The text-box trim ends the content box at the baseline, and a clamped
|
|
62899
|
+
// badge is clipped there: descenders of the last visible line would be
|
|
62900
|
+
// cut. Halfway to the next line's cap top keeps them, still above any
|
|
62901
|
+
// ink from the line the clamp hides.
|
|
62902
|
+
,
|
|
62903
|
+
|
|
62904
|
+
overflowClipMargin: "content-box calc((1lh - 1cap) / 2)",
|
|
62637
62905
|
...props,
|
|
62638
62906
|
styleCSSVars: BadgeStyleCSSVars,
|
|
62639
62907
|
spacing: jsx("span", {}),
|
|
@@ -62657,6 +62925,15 @@ const BadgeStyleCSSVars = {
|
|
|
62657
62925
|
fontSize: "--font-size"
|
|
62658
62926
|
};
|
|
62659
62927
|
const BadgeButton = props => {
|
|
62928
|
+
const ownTargetHidden = useOwnTargetHidden(props);
|
|
62929
|
+
if (ownTargetHidden) {
|
|
62930
|
+
return null;
|
|
62931
|
+
}
|
|
62932
|
+
return jsx(BadgeButtonUI, {
|
|
62933
|
+
...props
|
|
62934
|
+
});
|
|
62935
|
+
};
|
|
62936
|
+
const BadgeButtonUI = props => {
|
|
62660
62937
|
const defaultRef = useRef();
|
|
62661
62938
|
props.ref = props.ref || defaultRef;
|
|
62662
62939
|
const [buttonRootProps, buttonHostProps] = useControlProps(props, {
|
|
@@ -62687,20 +62964,208 @@ installImportMetaCssBuild(import.meta);const css$w = /* css */`
|
|
|
62687
62964
|
visibility: hidden;
|
|
62688
62965
|
pointer-events: none;
|
|
62689
62966
|
}
|
|
62967
|
+
|
|
62968
|
+
/* maxLines renders every badge for one layout, reads where the rows fell,
|
|
62969
|
+
then renders again with only what fits. The in-between is hidden rather
|
|
62970
|
+
than clipped: the badges that don't make it must leave the DOM, not sit
|
|
62971
|
+
there cut in half. Both renders land in the same frame (the second one
|
|
62972
|
+
is queued from a layout effect), so nothing shows up half measured. */
|
|
62973
|
+
&[navi-badge-list-measuring] {
|
|
62974
|
+
visibility: hidden;
|
|
62975
|
+
}
|
|
62690
62976
|
}
|
|
62691
62977
|
|
|
62692
62978
|
.navi_badge.navi_badge_more {
|
|
62693
62979
|
white-space: nowrap;
|
|
62694
62980
|
}
|
|
62695
62981
|
`;
|
|
62696
|
-
|
|
62982
|
+
|
|
62983
|
+
// Groups badges by the row they wrapped onto.
|
|
62984
|
+
// The signal we look for is horizontal: inside a row each badge starts further
|
|
62985
|
+
// right than the previous one, at a wrap the next one starts back at the row
|
|
62986
|
+
// start. Vertical positions can't be used because "align-items: center" gives
|
|
62987
|
+
// badges of different heights different tops within a single row.
|
|
62988
|
+
const groupRectsByRow = elements => {
|
|
62989
|
+
const rows = [];
|
|
62990
|
+
let previousLeft = -Infinity;
|
|
62991
|
+
for (const element of elements) {
|
|
62992
|
+
const rect = element.getBoundingClientRect();
|
|
62993
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
62994
|
+
continue;
|
|
62995
|
+
}
|
|
62996
|
+
if (rows.length === 0 || rect.left <= previousLeft) {
|
|
62997
|
+
rows.push([]);
|
|
62998
|
+
}
|
|
62999
|
+
rows[rows.length - 1].push(rect);
|
|
63000
|
+
previousLeft = rect.left;
|
|
63001
|
+
}
|
|
63002
|
+
return rows;
|
|
63003
|
+
};
|
|
63004
|
+
|
|
63005
|
+
// Reads a list that currently holds every badge plus the "+N" badge and tells
|
|
63006
|
+
// how many badges fit in maxLines rows.
|
|
63007
|
+
const measureRowFit = (listEl, maxLines) => {
|
|
63008
|
+
const elements = Array.from(listEl.children);
|
|
63009
|
+
if (elements.length < 2) {
|
|
63010
|
+
return elements.length;
|
|
63011
|
+
}
|
|
63012
|
+
// The "+N" badge is rendered last. It sits after every badge so it moves none
|
|
63013
|
+
// of them, which is what lets it be measured in the same layout it is left
|
|
63014
|
+
// out of.
|
|
63015
|
+
const moreRect = elements[elements.length - 1].getBoundingClientRect();
|
|
63016
|
+
const badgeElements = elements.slice(0, -1);
|
|
63017
|
+
const rows = groupRectsByRow(badgeElements);
|
|
63018
|
+
if (rows.length <= maxLines) {
|
|
63019
|
+
return badgeElements.length;
|
|
63020
|
+
}
|
|
63021
|
+
const styles = getComputedStyle(listEl);
|
|
63022
|
+
const gap = parseFloat(styles.columnGap) || 0;
|
|
63023
|
+
const contentRight = listEl.getBoundingClientRect().right - (parseFloat(styles.paddingRight) || 0) - (parseFloat(styles.borderRightWidth) || 0);
|
|
63024
|
+
|
|
63025
|
+
// The "+N" badge lands right after the last kept badge, so it eats into the
|
|
63026
|
+
// last visible row: drop badges from that row until it fits. Emptying that
|
|
63027
|
+
// row entirely is a valid outcome — the badge then wraps onto it and takes it
|
|
63028
|
+
// for itself, which is still within maxLines.
|
|
63029
|
+
const rowsKept = rows.slice(0, maxLines);
|
|
63030
|
+
const lastRow = rowsKept[rowsKept.length - 1];
|
|
63031
|
+
let lastRowCount = lastRow.length;
|
|
63032
|
+
while (lastRowCount > 0 && lastRow[lastRowCount - 1].right + gap + moreRect.width > contentRight + 0.5) {
|
|
63033
|
+
lastRowCount--;
|
|
63034
|
+
}
|
|
63035
|
+
return rowsKept.slice(0, -1).reduce((count, row) => count + row.length, 0) + lastRowCount;
|
|
63036
|
+
};
|
|
63037
|
+
const BADGE_LIST_PROPS = {
|
|
63038
|
+
inline: true,
|
|
63039
|
+
flex: "x",
|
|
63040
|
+
alignY: "center",
|
|
63041
|
+
spacing: "xs"
|
|
63042
|
+
};
|
|
63043
|
+
|
|
63044
|
+
// The badges hand themselves over as they render instead of being read upfront
|
|
63045
|
+
// from the children vnodes: see badge_list_context.js. Only worth it when the
|
|
63046
|
+
// list has something to decide — how many to show, or whether there is none at
|
|
63047
|
+
// all. Otherwise the badges render themselves and nothing has to be collected.
|
|
63048
|
+
const useBadgeRegistry = (children, enabled) => {
|
|
63049
|
+
const registryRef = useRef();
|
|
63050
|
+
const previousChildrenRef = useRef();
|
|
63051
|
+
if (!enabled) {
|
|
63052
|
+
return null;
|
|
63053
|
+
}
|
|
63054
|
+
const registry = registryRef.current || (registryRef.current = createBadgeRegistry());
|
|
63055
|
+
// Whether Preact is about to render the badges again or hand back the ones it
|
|
63056
|
+
// already has, which it does when the list re-renders on its own.
|
|
63057
|
+
registry.startPass(previousChildrenRef.current !== children);
|
|
63058
|
+
previousChildrenRef.current = children;
|
|
63059
|
+
return registry;
|
|
63060
|
+
};
|
|
63061
|
+
|
|
63062
|
+
/**
|
|
63063
|
+
* A row of badges that wraps.
|
|
63064
|
+
*
|
|
63065
|
+
* Four shapes behind one name, because what the list has to do at runtime is
|
|
63066
|
+
* not the same in each. Nothing is set up for a case that cannot happen: a
|
|
63067
|
+
* plain list is one element holding its children as-is — no registry, no
|
|
63068
|
+
* effect —, a capped one collects its badges but measures nothing, and only
|
|
63069
|
+
* shrinkWrap ever builds the measurement ghost.
|
|
63070
|
+
*
|
|
63071
|
+
* @param {import("ignore:preact").ComponentChildren} [fallback]
|
|
63072
|
+
* Rendered in place of the badges when there is none. Without it an empty
|
|
63073
|
+
* list renders nothing at all. In a <Picker> this is the only placeholder
|
|
63074
|
+
* the user gets — a picker given a `ui` does not draw its own — so pass the
|
|
63075
|
+
* placeholder text: `fallback="Select skills…"`. Plain text is the right
|
|
63076
|
+
* choice there: it reads at the picker's size, in the placeholder color the
|
|
63077
|
+
* picker gives its empty value slot, and the box stays the same height. A
|
|
63078
|
+
* transparent <Badge> matches the slot to the pixel instead, at the price of
|
|
63079
|
+
* badge-sized text. See docs/badge_list.md.
|
|
63080
|
+
* @param {boolean} [shrinkWrap]
|
|
63081
|
+
* Narrows the list down to its widest row so the last row isn't ragged.
|
|
63082
|
+
* Defaults to true inside a <Picker> — the trigger draws a border around the
|
|
63083
|
+
* list, so the ragged edge shows — and false elsewhere, where the work would
|
|
63084
|
+
* often go unseen: opt in where an edge is visible. Ignored when maxLines is
|
|
63085
|
+
* in play, which needs the full width to know where the rows fall.
|
|
63086
|
+
* @param {number} [max]
|
|
63087
|
+
* Caps how many badges are rendered; the surplus becomes a "+N" badge, which
|
|
63088
|
+
* takes one of the max slots.
|
|
63089
|
+
* @param {number} [maxLines]
|
|
63090
|
+
* Caps how many rows are shown, measured. Falls back to what the surrounding
|
|
63091
|
+
* component grants (a <Picker>, see max_lines_context.js).
|
|
63092
|
+
*/
|
|
63093
|
+
const BadgeList = props => {
|
|
63094
|
+
import.meta.css = [css$w, "@jsenv/navi/src/text/badge_list.jsx"];
|
|
63095
|
+
const {
|
|
63096
|
+
maxLines,
|
|
63097
|
+
max,
|
|
63098
|
+
fallback
|
|
63099
|
+
} = props;
|
|
63100
|
+
const maxLinesFromAbove = useContext(MaxLinesContext);
|
|
63101
|
+
const maxLinesResolved = maxLines === undefined ? maxLinesFromAbove : maxLines;
|
|
63102
|
+
// Granted lines mean a clamping container — a <Picker> — whose border makes
|
|
63103
|
+
// the ragged last row show; nothing of the sort around us, and shrink
|
|
63104
|
+
// wrapping is work nobody sees unless asked for.
|
|
63105
|
+
const {
|
|
63106
|
+
shrinkWrap = maxLinesFromAbove !== undefined
|
|
63107
|
+
} = props;
|
|
63108
|
+
if (maxLinesResolved !== undefined) {
|
|
63109
|
+
// shrinkWrap is dropped on purpose: it narrows the list down to its widest
|
|
63110
|
+
// row, which would re-wrap the badges under the cap just measured.
|
|
63111
|
+
return jsx(BadgeListMaxLines, {
|
|
63112
|
+
...props,
|
|
63113
|
+
maxLines: maxLinesResolved
|
|
63114
|
+
});
|
|
63115
|
+
}
|
|
63116
|
+
if (shrinkWrap) {
|
|
63117
|
+
return jsx(BadgeListShrinkWrap, {
|
|
63118
|
+
...props
|
|
63119
|
+
});
|
|
63120
|
+
}
|
|
63121
|
+
if (max !== undefined || fallback !== undefined) {
|
|
63122
|
+
return jsx(BadgeListCounted, {
|
|
63123
|
+
...props
|
|
63124
|
+
});
|
|
63125
|
+
}
|
|
63126
|
+
return jsx(BadgeListPlain, {
|
|
63127
|
+
...props
|
|
63128
|
+
});
|
|
63129
|
+
};
|
|
63130
|
+
|
|
63131
|
+
// Nothing to measure, cap, or count: the badges render themselves — no
|
|
63132
|
+
// registry, no context, no effect, one element.
|
|
63133
|
+
const BadgeListPlain = props => {
|
|
63134
|
+
return jsx(Box, {
|
|
63135
|
+
baseClassName: "navi_badge_list",
|
|
63136
|
+
...BADGE_LIST_PROPS,
|
|
63137
|
+
...props
|
|
63138
|
+
});
|
|
63139
|
+
};
|
|
63140
|
+
|
|
63141
|
+
// max/fallback need the badge count, so the badges are collected — but nothing
|
|
63142
|
+
// is measured and no DOM is watched.
|
|
63143
|
+
const BadgeListCounted = ({
|
|
62697
63144
|
fallback,
|
|
62698
63145
|
children,
|
|
62699
|
-
shrinkWrap = true,
|
|
62700
63146
|
max,
|
|
62701
|
-
...
|
|
63147
|
+
...boxProps
|
|
62702
63148
|
}) => {
|
|
62703
|
-
|
|
63149
|
+
const registry = useBadgeRegistry(children, true);
|
|
63150
|
+
return jsx(Box, {
|
|
63151
|
+
baseClassName: "navi_badge_list",
|
|
63152
|
+
...BADGE_LIST_PROPS,
|
|
63153
|
+
...boxProps,
|
|
63154
|
+
children: jsx(BadgeListChildren, {
|
|
63155
|
+
registry: registry,
|
|
63156
|
+
max: max,
|
|
63157
|
+
fallback: fallback,
|
|
63158
|
+
children: children
|
|
63159
|
+
})
|
|
63160
|
+
});
|
|
63161
|
+
};
|
|
63162
|
+
const BadgeListShrinkWrap = ({
|
|
63163
|
+
fallback,
|
|
63164
|
+
children,
|
|
63165
|
+
max,
|
|
63166
|
+
...restProps
|
|
63167
|
+
}) => {
|
|
63168
|
+
const registry = useBadgeRegistry(children, max !== undefined || fallback !== undefined);
|
|
62704
63169
|
const measureRef = useRef();
|
|
62705
63170
|
const visibleRef = useRef();
|
|
62706
63171
|
useLayoutEffect(() => {
|
|
@@ -62713,18 +63178,25 @@ const BadgeList = ({
|
|
|
62713
63178
|
let rafId;
|
|
62714
63179
|
const measure = () => {
|
|
62715
63180
|
visibleEl.style.width = "";
|
|
62716
|
-
|
|
62717
|
-
|
|
62718
|
-
|
|
62719
|
-
|
|
62720
|
-
|
|
62721
|
-
|
|
62722
|
-
|
|
62723
|
-
|
|
62724
|
-
visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
|
|
62725
|
-
}
|
|
63181
|
+
// Clone the already-rendered DOM nodes instead of letting React/Preact
|
|
63182
|
+
// render the children a second time into the ghost: re-rendering would
|
|
63183
|
+
// instantiate Badge/Badge.Button a second time, double-registering their
|
|
63184
|
+
// controllers (and any other mount side effect) under the same id.
|
|
63185
|
+
measureEl.replaceChildren(...Array.from(visibleEl.children, child => child.cloneNode(true)));
|
|
63186
|
+
const optimalWidth = measureWidestChildRow(measureEl);
|
|
63187
|
+
if (optimalWidth !== null) {
|
|
63188
|
+
visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
|
|
62726
63189
|
}
|
|
62727
63190
|
};
|
|
63191
|
+
|
|
63192
|
+
// A single badge is a single row, and a single row is already the widest
|
|
63193
|
+
// one: there is nothing to narrow, and no width the list could be given
|
|
63194
|
+
// that would change that.
|
|
63195
|
+
if (visibleEl.children.length < 2) {
|
|
63196
|
+
visibleEl.style.width = "";
|
|
63197
|
+
measureEl.replaceChildren();
|
|
63198
|
+
return undefined;
|
|
63199
|
+
}
|
|
62728
63200
|
measure();
|
|
62729
63201
|
const onResize = () => {
|
|
62730
63202
|
cancelAnimationFrame(rafId);
|
|
@@ -62741,36 +63213,201 @@ const BadgeList = ({
|
|
|
62741
63213
|
observer?.disconnect();
|
|
62742
63214
|
window.removeEventListener("resize", onResize);
|
|
62743
63215
|
};
|
|
62744
|
-
}, [
|
|
62745
|
-
const
|
|
62746
|
-
|
|
62747
|
-
|
|
62748
|
-
const hiddenCount = hasMax ? childArray.length - (max - 1) : 0;
|
|
62749
|
-
const sharedProps = {
|
|
62750
|
-
inline: true,
|
|
62751
|
-
flex: "x",
|
|
62752
|
-
alignY: "center",
|
|
62753
|
-
spacing: "xs",
|
|
62754
|
-
...props
|
|
63216
|
+
}, [children]);
|
|
63217
|
+
const boxProps = {
|
|
63218
|
+
...BADGE_LIST_PROPS,
|
|
63219
|
+
...restProps
|
|
62755
63220
|
};
|
|
62756
|
-
return
|
|
62757
|
-
|
|
62758
|
-
|
|
62759
|
-
|
|
62760
|
-
|
|
62761
|
-
|
|
62762
|
-
|
|
62763
|
-
|
|
62764
|
-
|
|
62765
|
-
|
|
62766
|
-
|
|
62767
|
-
|
|
62768
|
-
|
|
62769
|
-
|
|
62770
|
-
|
|
62771
|
-
|
|
63221
|
+
return (
|
|
63222
|
+
// inline flex, not a plain block: the wrapper must sit on the line the way
|
|
63223
|
+
// the list itself would, otherwise the list lands a pixel low in some
|
|
63224
|
+
// containers.
|
|
63225
|
+
jsxs(Box, {
|
|
63226
|
+
relative: true,
|
|
63227
|
+
inline: true,
|
|
63228
|
+
flex: "x",
|
|
63229
|
+
children: [jsx(Box, {
|
|
63230
|
+
baseClassName: "navi_badge_list",
|
|
63231
|
+
...boxProps,
|
|
63232
|
+
ref: measureRef,
|
|
63233
|
+
"aria-hidden": "true",
|
|
63234
|
+
"navi-badge-list-clone": ""
|
|
63235
|
+
}), jsx(Box, {
|
|
63236
|
+
baseClassName: "navi_badge_list",
|
|
63237
|
+
...boxProps,
|
|
63238
|
+
ref: visibleRef,
|
|
63239
|
+
children: jsx(BadgeListChildren, {
|
|
63240
|
+
registry: registry,
|
|
63241
|
+
max: max,
|
|
63242
|
+
fallback: fallback,
|
|
63243
|
+
children: children
|
|
62772
63244
|
})
|
|
62773
63245
|
})]
|
|
63246
|
+
})
|
|
63247
|
+
);
|
|
63248
|
+
};
|
|
63249
|
+
const BadgeListMaxLines = ({
|
|
63250
|
+
fallback,
|
|
63251
|
+
children,
|
|
63252
|
+
max,
|
|
63253
|
+
maxLines,
|
|
63254
|
+
...boxProps
|
|
63255
|
+
}) => {
|
|
63256
|
+
const registry = useBadgeRegistry(children, true);
|
|
63257
|
+
const visibleRef = useRef();
|
|
63258
|
+
|
|
63259
|
+
// What the measure found: how many badges there were and how many of them fit
|
|
63260
|
+
// in maxLines rows. null means "not measured yet": the list then renders them
|
|
63261
|
+
// all, hidden, for the layout effect to read.
|
|
63262
|
+
const [fit, setFit] = useState(null);
|
|
63263
|
+
// The two widths this list gives whatever is around it: the one it takes with
|
|
63264
|
+
// every badge rendered, and the one it settles on once the surplus is gone.
|
|
63265
|
+
// Neither is a reason to measure again — see the resize watch below.
|
|
63266
|
+
const selfWidthsRef = useRef({
|
|
63267
|
+
full: -1,
|
|
63268
|
+
settled: -1
|
|
63269
|
+
});
|
|
63270
|
+
const measuring = fit === null;
|
|
63271
|
+
// A single badge is a single row whatever the width, so nothing can move it
|
|
63272
|
+
// out of the cap and nothing has to be watched.
|
|
63273
|
+
const watchesResize = fit !== null && fit.count > 1;
|
|
63274
|
+
|
|
63275
|
+
// Runs after every render, which is when the badges have registered and the
|
|
63276
|
+
// DOM holds whatever this render asked for.
|
|
63277
|
+
useLayoutEffect(() => {
|
|
63278
|
+
const visibleEl = visibleRef.current;
|
|
63279
|
+
if (!visibleEl) {
|
|
63280
|
+
return;
|
|
63281
|
+
}
|
|
63282
|
+
const count = registry.getEntries().length;
|
|
63283
|
+
const width = visibleEl.parentElement?.getBoundingClientRect().width ?? -1;
|
|
63284
|
+
if (fit === null) {
|
|
63285
|
+
selfWidthsRef.current.full = width;
|
|
63286
|
+
setFit({
|
|
63287
|
+
count,
|
|
63288
|
+
shown: count < 2 ? count : measureRowFit(visibleEl, maxLines)
|
|
63289
|
+
});
|
|
63290
|
+
return;
|
|
63291
|
+
}
|
|
63292
|
+
selfWidthsRef.current.settled = width;
|
|
63293
|
+
if (fit.count !== count) {
|
|
63294
|
+
// Badges came or went: what was measured no longer describes them.
|
|
63295
|
+
setFit(null);
|
|
63296
|
+
}
|
|
63297
|
+
});
|
|
63298
|
+
useLayoutEffect(() => {
|
|
63299
|
+
const outerParent = visibleRef.current?.parentElement;
|
|
63300
|
+
if (!watchesResize || !outerParent) {
|
|
63301
|
+
return undefined;
|
|
63302
|
+
}
|
|
63303
|
+
let rafId;
|
|
63304
|
+
const remeasure = () => {
|
|
63305
|
+
// Only a width change can move the rows — a height change is this list
|
|
63306
|
+
// growing or shrinking, never the room it was given.
|
|
63307
|
+
//
|
|
63308
|
+
// And not every width change either. Nothing guarantees an ancestor whose
|
|
63309
|
+
// width does not follow its content (a column with align-items: start
|
|
63310
|
+
// sizes every row to what is inside it), so rendering every badge widens
|
|
63311
|
+
// what is being watched and dropping the surplus narrows it right back.
|
|
63312
|
+
// Those two widths are this list talking to itself; measuring again on
|
|
63313
|
+
// them never ends. Any other width is the room around it changing.
|
|
63314
|
+
const width = outerParent.getBoundingClientRect().width;
|
|
63315
|
+
const {
|
|
63316
|
+
full,
|
|
63317
|
+
settled
|
|
63318
|
+
} = selfWidthsRef.current;
|
|
63319
|
+
if (Math.abs(width - full) < 0.5 || Math.abs(width - settled) < 0.5) {
|
|
63320
|
+
return;
|
|
63321
|
+
}
|
|
63322
|
+
cancelAnimationFrame(rafId);
|
|
63323
|
+
rafId = requestAnimationFrame(() => setFit(null));
|
|
63324
|
+
};
|
|
63325
|
+
const observer = new ResizeObserver(remeasure);
|
|
63326
|
+
observer.observe(outerParent);
|
|
63327
|
+
window.addEventListener("resize", remeasure);
|
|
63328
|
+
return () => {
|
|
63329
|
+
cancelAnimationFrame(rafId);
|
|
63330
|
+
observer.disconnect();
|
|
63331
|
+
window.removeEventListener("resize", remeasure);
|
|
63332
|
+
};
|
|
63333
|
+
}, [watchesResize, maxLines]);
|
|
63334
|
+
return jsx(Box, {
|
|
63335
|
+
baseClassName: "navi_badge_list",
|
|
63336
|
+
...BADGE_LIST_PROPS,
|
|
63337
|
+
...boxProps,
|
|
63338
|
+
ref: visibleRef,
|
|
63339
|
+
"navi-badge-list-measuring": measuring ? "" : undefined,
|
|
63340
|
+
children: jsx(BadgeListChildren, {
|
|
63341
|
+
registry: registry,
|
|
63342
|
+
max: max,
|
|
63343
|
+
fallback: fallback,
|
|
63344
|
+
measuring: measuring,
|
|
63345
|
+
rowFit: fit?.shown ?? null,
|
|
63346
|
+
children: children
|
|
63347
|
+
})
|
|
63348
|
+
});
|
|
63349
|
+
};
|
|
63350
|
+
const BadgeListChildren = ({
|
|
63351
|
+
registry,
|
|
63352
|
+
children,
|
|
63353
|
+
fallback,
|
|
63354
|
+
max,
|
|
63355
|
+
measuring,
|
|
63356
|
+
rowFit
|
|
63357
|
+
}) => {
|
|
63358
|
+
if (!registry) {
|
|
63359
|
+
// The badges are on their own: nothing here decides which of them render.
|
|
63360
|
+
return children;
|
|
63361
|
+
}
|
|
63362
|
+
return jsxs(Fragment$1, {
|
|
63363
|
+
children: [jsx(BadgeListContext.Provider, {
|
|
63364
|
+
value: registry,
|
|
63365
|
+
children: children
|
|
63366
|
+
}), jsx(BadgeListContent, {
|
|
63367
|
+
registry: registry,
|
|
63368
|
+
fallback: fallback,
|
|
63369
|
+
max: max,
|
|
63370
|
+
measuring: measuring,
|
|
63371
|
+
rowFit: rowFit
|
|
63372
|
+
})]
|
|
63373
|
+
});
|
|
63374
|
+
};
|
|
63375
|
+
const BadgeListContent = ({
|
|
63376
|
+
registry,
|
|
63377
|
+
fallback,
|
|
63378
|
+
max,
|
|
63379
|
+
measuring,
|
|
63380
|
+
rowFit
|
|
63381
|
+
}) => {
|
|
63382
|
+
const entries = registry.getEntries();
|
|
63383
|
+
const count = entries.length;
|
|
63384
|
+
if (count === 0) {
|
|
63385
|
+
return fallback;
|
|
63386
|
+
}
|
|
63387
|
+
|
|
63388
|
+
// The "+N" badge stands among the badges, so it takes one of the max slots
|
|
63389
|
+
// when there is a surplus to name. A list of exactly `max` badges has nothing
|
|
63390
|
+
// to name and keeps all of them.
|
|
63391
|
+
let shownCount = max !== undefined && count > max ? max - 1 : count;
|
|
63392
|
+
if (!measuring && rowFit !== null && rowFit !== undefined && rowFit < shownCount) {
|
|
63393
|
+
shownCount = rowFit;
|
|
63394
|
+
}
|
|
63395
|
+
// While measuring, everything above is on screen (hidden) along with the "+N"
|
|
63396
|
+
// badge, so the layout effect can see where the rows fall and how much room
|
|
63397
|
+
// that badge asks for. Its label then reads the worst case — every badge
|
|
63398
|
+
// hidden — so the room reserved is never short.
|
|
63399
|
+
const hasMore = measuring || shownCount < count;
|
|
63400
|
+
return jsxs(Fragment$1, {
|
|
63401
|
+
children: [entries.slice(0, shownCount).map((badgeProps, index) =>
|
|
63402
|
+
// Keyed by position: a badge's own key went to the registering vnode
|
|
63403
|
+
// above and doesn't reach here, and badges keep no state worth moving.
|
|
63404
|
+
jsx(BadgeUI, {
|
|
63405
|
+
...badgeProps
|
|
63406
|
+
}, index)), hasMore && jsx(BadgeUI, {
|
|
63407
|
+
className: "navi_badge_more",
|
|
63408
|
+
children: naviI18n("badge_list.more", {
|
|
63409
|
+
count: measuring ? count : count - shownCount
|
|
63410
|
+
})
|
|
62774
63411
|
})]
|
|
62775
63412
|
});
|
|
62776
63413
|
};
|
|
@@ -63001,7 +63638,6 @@ const PickerObjectUI = () => {
|
|
|
63001
63638
|
const PickerArray = props => {
|
|
63002
63639
|
const Next = useNextResolver();
|
|
63003
63640
|
return jsx(Next, {
|
|
63004
|
-
maxLines: "3",
|
|
63005
63641
|
ui: jsx(PickerArrayUI, {}),
|
|
63006
63642
|
...props,
|
|
63007
63643
|
type: "navi_js",
|
|
@@ -63031,6 +63667,57 @@ const PickerArrayUI = () => {
|
|
|
63031
63667
|
})
|
|
63032
63668
|
});
|
|
63033
63669
|
};
|
|
63670
|
+
|
|
63671
|
+
/**
|
|
63672
|
+
* One value the picker holds, drawn as a chip with a cross that takes it back
|
|
63673
|
+
* out. Sits wherever the application draws what was picked — on the picker's
|
|
63674
|
+
* façade (`ui`) or inside its popup — and both behave the same.
|
|
63675
|
+
*
|
|
63676
|
+
* The cross asks with `--navi-unselect` rather than writing a new list, and it
|
|
63677
|
+
* asks the picker — which holds what was picked, and hands it down to whatever
|
|
63678
|
+
* draws it in the popup. Nothing to name: the picker is the nearest control
|
|
63679
|
+
* around the chip. `commandFor` is for a chip that stands outside the picker it
|
|
63680
|
+
* speaks for.
|
|
63681
|
+
*
|
|
63682
|
+
* The cross is an own target (see own_target.js), so the press belongs to it
|
|
63683
|
+
* and not to the picker underneath, and it goes when the picker turns read-only
|
|
63684
|
+
* — a row being read still says what was picked, it just no longer offers to
|
|
63685
|
+
* unpick it.
|
|
63686
|
+
*
|
|
63687
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
63688
|
+
* value: any,
|
|
63689
|
+
* commandFor?: string,
|
|
63690
|
+
* children?: import("ignore:preact").ComponentChildren,
|
|
63691
|
+
* [key: string]: any,
|
|
63692
|
+
* }>}
|
|
63693
|
+
* @param {any} value The value this chip stands for — one entry of what the
|
|
63694
|
+
* picker holds, and what `--navi-unselect` carries.
|
|
63695
|
+
* @param {string} [commandFor] The id of the picker to take the value out of,
|
|
63696
|
+
* when the chip does not sit inside it.
|
|
63697
|
+
*/
|
|
63698
|
+
const PickerChip = ({
|
|
63699
|
+
value,
|
|
63700
|
+
commandFor,
|
|
63701
|
+
children,
|
|
63702
|
+
...rest
|
|
63703
|
+
}) => {
|
|
63704
|
+
return jsxs(Badge, {
|
|
63705
|
+
inline: true,
|
|
63706
|
+
flex: true,
|
|
63707
|
+
...rest,
|
|
63708
|
+
children: [children, jsx(Badge.Button, {
|
|
63709
|
+
ownTarget: true,
|
|
63710
|
+
command: "--navi-unselect",
|
|
63711
|
+
commandFor: commandFor,
|
|
63712
|
+
value: value,
|
|
63713
|
+
"aria-label": naviI18n("button.remove"),
|
|
63714
|
+
children: jsx(Icon, {
|
|
63715
|
+
lineOverflow: "allow",
|
|
63716
|
+
children: jsx(CloseSvg, {})
|
|
63717
|
+
})
|
|
63718
|
+
})]
|
|
63719
|
+
});
|
|
63720
|
+
};
|
|
63034
63721
|
const PickerColor = props => {
|
|
63035
63722
|
const Next = useNextResolver();
|
|
63036
63723
|
return jsx(Next, {
|
|
@@ -63469,6 +64156,30 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
|
|
|
63469
64156
|
&[navi-placeholder] {
|
|
63470
64157
|
color: var(--picker-placeholder-color);
|
|
63471
64158
|
}
|
|
64159
|
+
|
|
64160
|
+
/* A <BadgeList> caps its own rows: it renders the badges that fit and a
|
|
64161
|
+
"+N" badge for the rest, reading the number from MaxLinesContext right
|
|
64162
|
+
below. The picker must then not clamp on top of it — line-clamp turns
|
|
64163
|
+
the value into a -webkit-box and single-line truncation into a
|
|
64164
|
+
nowrap block, either of which takes the badge list out of the
|
|
64165
|
+
inline-flex layout it needs. maxLines writes those as inline styles on
|
|
64166
|
+
the element, hence !important. */
|
|
64167
|
+
&:has(.navi_badge_list) {
|
|
64168
|
+
display: inline-flex !important;
|
|
64169
|
+
-webkit-line-clamp: none !important;
|
|
64170
|
+
overflow: visible !important;
|
|
64171
|
+
-webkit-box-orient: horizontal !important;
|
|
64172
|
+
text-overflow: clip !important;
|
|
64173
|
+
white-space: normal !important;
|
|
64174
|
+
}
|
|
64175
|
+
|
|
64176
|
+
/* The façade is transparent to the pointer — a press on what it draws
|
|
64177
|
+
means "open the picker". An own target is the exception, the same way
|
|
64178
|
+
the clear cross is one in the slot below: it says the press is aimed at
|
|
64179
|
+
IT, so it has to be reachable at all. */
|
|
64180
|
+
[data-navi-own-target] {
|
|
64181
|
+
pointer-events: auto;
|
|
64182
|
+
}
|
|
63472
64183
|
}
|
|
63473
64184
|
.navi_picker_right_slot {
|
|
63474
64185
|
display: inline-flex;
|
|
@@ -63757,214 +64468,284 @@ const PickerButton = props => {
|
|
|
63757
64468
|
// clearing being a modification like any other.
|
|
63758
64469
|
const interactive = !basePseudoState[":disabled"] && !basePseudoState[":read-only"] && !loading;
|
|
63759
64470
|
usePickerErrorCallout(uiStateController, error);
|
|
63760
|
-
return
|
|
63761
|
-
|
|
63762
|
-
|
|
63763
|
-
|
|
63764
|
-
|
|
63765
|
-
|
|
63766
|
-
|
|
63767
|
-
,
|
|
64471
|
+
return (
|
|
64472
|
+
/* Read-only crosses into everything the picker is made of: what it really
|
|
64473
|
+
holds is drawn by controls of their own — in the popup, and on the façade
|
|
64474
|
+
where an application puts its own affordances — and each of them refuses
|
|
64475
|
+
in its own words once told. Said from the read-only state alone, never
|
|
64476
|
+
from the busy one — an action running for a moment is not the same thing
|
|
64477
|
+
as a value nobody may change. */
|
|
64478
|
+
jsx(ReadOnlyContext.Provider, {
|
|
64479
|
+
value: readOnlyResolved,
|
|
64480
|
+
children: jsxs(Box, {
|
|
64481
|
+
as: "div",
|
|
64482
|
+
ref: ref
|
|
64483
|
+
// The flow this element really has (.navi_picker is display:inline-flex).
|
|
64484
|
+
// Left unsaid, Box reads a <div> as block and resolves alignX into a
|
|
64485
|
+
// text-align — which is a different intention entirely (that one is the
|
|
64486
|
+
// textAlign prop, placing the text INSIDE the value slot).
|
|
64487
|
+
,
|
|
63768
64488
|
|
|
63769
|
-
|
|
63770
|
-
|
|
63771
|
-
|
|
63772
|
-
|
|
63773
|
-
|
|
63774
|
-
|
|
63775
|
-
|
|
63776
|
-
|
|
63777
|
-
|
|
63778
|
-
|
|
63779
|
-
|
|
63780
|
-
|
|
63781
|
-
|
|
63782
|
-
|
|
63783
|
-
|
|
63784
|
-
|
|
63785
|
-
|
|
63786
|
-
|
|
63787
|
-
|
|
63788
|
-
|
|
63789
|
-
|
|
63790
|
-
|
|
63791
|
-
|
|
63792
|
-
|
|
63793
|
-
|
|
63794
|
-
|
|
63795
|
-
|
|
63796
|
-
|
|
64489
|
+
inline: true,
|
|
64490
|
+
flex: "x",
|
|
64491
|
+
baseClassName: "navi_picker",
|
|
64492
|
+
pseudoClasses: PICKER_BUTTON_PSEUDO_CLASSES,
|
|
64493
|
+
"data-variant": variant,
|
|
64494
|
+
"navi-picker": "",
|
|
64495
|
+
"navi-single-line": isSingleLine ? "" : undefined,
|
|
64496
|
+
"navi-ui-custom": ui === "default" ? undefined : "",
|
|
64497
|
+
"data-readonly-opens": readOnlyOpens ? "" : undefined,
|
|
64498
|
+
"data-popup-width-fit-content": popupWidthFitContent ? "" : undefined,
|
|
64499
|
+
...pickerRemainingProps,
|
|
64500
|
+
basePseudoState: basePseudoState,
|
|
64501
|
+
styleCSSVars: PickerStyleCSSVars,
|
|
64502
|
+
variant: undefined,
|
|
64503
|
+
rightSlotIcon: undefined,
|
|
64504
|
+
rightSlotIconSize: undefined,
|
|
64505
|
+
rightSlot: undefined,
|
|
64506
|
+
clearConfirm: undefined,
|
|
64507
|
+
clearConfirmPopupContent: undefined,
|
|
64508
|
+
openWhileReadOnly: undefined,
|
|
64509
|
+
ui: undefined,
|
|
64510
|
+
maxLines: undefined,
|
|
64511
|
+
popupWidthFitContent: undefined,
|
|
64512
|
+
error: undefined,
|
|
64513
|
+
dayLabel: undefined
|
|
64514
|
+
// This wrapper will receive keyboard event bubbling from the picker popup content
|
|
64515
|
+
// we re-dispatch on the input (to get escape to close for instance)
|
|
64516
|
+
,
|
|
63797
64517
|
|
|
63798
|
-
|
|
63799
|
-
|
|
63800
|
-
|
|
63801
|
-
|
|
64518
|
+
onKeyDown: inputProps.onKeyDown
|
|
64519
|
+
// in case request open/close are dispatched on the control root ->
|
|
64520
|
+
// redispatch them to the host
|
|
64521
|
+
,
|
|
63802
64522
|
|
|
63803
|
-
|
|
63804
|
-
|
|
63805
|
-
|
|
63806
|
-
|
|
63807
|
-
|
|
63808
|
-
|
|
63809
|
-
|
|
63810
|
-
|
|
63811
|
-
|
|
63812
|
-
|
|
63813
|
-
|
|
63814
|
-
...inputProps,
|
|
63815
|
-
// eslint-disable-next-line react/no-children-prop
|
|
63816
|
-
children: undefined // we will render children into the div
|
|
64523
|
+
onnavi_request_open: inputProps.onnavi_request_open,
|
|
64524
|
+
onnavi_request_close: inputProps.onnavi_request_close
|
|
64525
|
+
// `--navi-select`/`--navi-unselect` about one entry of the list the
|
|
64526
|
+
// picker holds — a chip on the façade, a suggestion beside the field.
|
|
64527
|
+
// Answered here rather than by the control drawing that list in the
|
|
64528
|
+
// popup, even though rows are what such a control owns: a picker given
|
|
64529
|
+
// its own value builds its popup only on first open (see
|
|
64530
|
+
// popup_content_mount.js), so before that there is no such control at
|
|
64531
|
+
// all — and building the whole popup to have someone to talk to, for a
|
|
64532
|
+
// cross, is the wrong price. The picker holds the value in the first
|
|
64533
|
+
// place and hands it down whenever the popup is built.
|
|
63817
64534
|
,
|
|
63818
64535
|
|
|
63819
|
-
|
|
63820
|
-
|
|
63821
|
-
inputProps.onFocus?.(e);
|
|
63822
|
-
e.target.select();
|
|
64536
|
+
onnavi_request_select: e => {
|
|
64537
|
+
requestPickerListEntry(ref.current, inputRef.current, e, "select");
|
|
63823
64538
|
},
|
|
63824
|
-
|
|
63825
|
-
|
|
63826
|
-
if (isWithinPickerContent(e.target, pickerEl)) {
|
|
63827
|
-
return;
|
|
63828
|
-
}
|
|
63829
|
-
const uiState = uiStateController.uiState;
|
|
63830
|
-
if (uiState === undefined) {
|
|
63831
|
-
return;
|
|
63832
|
-
}
|
|
63833
|
-
e.preventDefault();
|
|
63834
|
-
const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
|
|
63835
|
-
e.clipboardData.setData("text/plain", displayText);
|
|
63836
|
-
e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
|
|
64539
|
+
onnavi_request_unselect: e => {
|
|
64540
|
+
requestPickerListEntry(ref.current, inputRef.current, e, "unselect");
|
|
63837
64541
|
},
|
|
63838
|
-
|
|
63839
|
-
|
|
63840
|
-
|
|
63841
|
-
|
|
63842
|
-
|
|
63843
|
-
|
|
63844
|
-
|
|
63845
|
-
|
|
63846
|
-
|
|
63847
|
-
|
|
63848
|
-
|
|
63849
|
-
|
|
63850
|
-
|
|
63851
|
-
|
|
63852
|
-
|
|
63853
|
-
|
|
63854
|
-
|
|
63855
|
-
|
|
63856
|
-
|
|
63857
|
-
|
|
63858
|
-
|
|
63859
|
-
|
|
63860
|
-
|
|
63861
|
-
|
|
63862
|
-
|
|
63863
|
-
|
|
63864
|
-
|
|
63865
|
-
|
|
63866
|
-
|
|
63867
|
-
|
|
63868
|
-
|
|
63869
|
-
|
|
63870
|
-
|
|
63871
|
-
|
|
63872
|
-
|
|
63873
|
-
|
|
63874
|
-
|
|
63875
|
-
|
|
63876
|
-
|
|
63877
|
-
|
|
63878
|
-
|
|
63879
|
-
|
|
63880
|
-
|
|
63881
|
-
|
|
63882
|
-
|
|
63883
|
-
|
|
64542
|
+
children: [jsxs("span", {
|
|
64543
|
+
className: "navi_picker_box",
|
|
64544
|
+
children: [variant === "headless" ? null : jsx(LoadingOutline, {
|
|
64545
|
+
loading: loading,
|
|
64546
|
+
color: "var(--picker-loader-color)",
|
|
64547
|
+
inset: -2
|
|
64548
|
+
}), jsx(PickerInput, {
|
|
64549
|
+
tabIndex: variant === "headless" ? -1 : undefined,
|
|
64550
|
+
"aria-hidden": variant === "headless" ? "true" : undefined,
|
|
64551
|
+
...inputProps,
|
|
64552
|
+
// eslint-disable-next-line react/no-children-prop
|
|
64553
|
+
children: undefined // we will render children into the div
|
|
64554
|
+
,
|
|
64555
|
+
|
|
64556
|
+
ui: ui,
|
|
64557
|
+
onFocus: e => {
|
|
64558
|
+
inputProps.onFocus?.(e);
|
|
64559
|
+
e.target.select();
|
|
64560
|
+
},
|
|
64561
|
+
onCopy: e => {
|
|
64562
|
+
const pickerEl = ref.current;
|
|
64563
|
+
if (isWithinPickerContent(e.target, pickerEl)) {
|
|
64564
|
+
return;
|
|
64565
|
+
}
|
|
64566
|
+
const uiState = uiStateController.uiState;
|
|
64567
|
+
if (uiState === undefined) {
|
|
64568
|
+
return;
|
|
64569
|
+
}
|
|
64570
|
+
e.preventDefault();
|
|
64571
|
+
const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
|
|
64572
|
+
e.clipboardData.setData("text/plain", displayText);
|
|
64573
|
+
e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
|
|
64574
|
+
},
|
|
64575
|
+
onCut: e => {
|
|
64576
|
+
const pickerEl = ref.current;
|
|
64577
|
+
if (isWithinPickerContent(e.target, pickerEl)) {
|
|
64578
|
+
return;
|
|
64579
|
+
}
|
|
64580
|
+
const uiState = uiStateController.uiState;
|
|
64581
|
+
if (uiState === undefined) {
|
|
64582
|
+
return;
|
|
64583
|
+
}
|
|
64584
|
+
// the copy part don't need control to be interactable
|
|
64585
|
+
const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
|
|
64586
|
+
e.clipboardData.setData("text/plain", displayText);
|
|
64587
|
+
e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
|
|
64588
|
+
// the clear ui state part need control to be interactable
|
|
64589
|
+
dispatchRequestInteraction(pickerEl, {
|
|
64590
|
+
event: e,
|
|
64591
|
+
name: "cut",
|
|
64592
|
+
allowed: () => {
|
|
64593
|
+
dispatchRequestClearUIState(inputRef.current, e);
|
|
64594
|
+
}
|
|
64595
|
+
});
|
|
64596
|
+
e.preventDefault();
|
|
64597
|
+
},
|
|
64598
|
+
onPaste: e => {
|
|
64599
|
+
const pickerEl = ref.current;
|
|
64600
|
+
if (isWithinPickerContent(e.target, pickerEl)) {
|
|
64601
|
+
// Don't intercept inside the picker popup content.
|
|
64602
|
+
return;
|
|
64603
|
+
}
|
|
64604
|
+
const naviData = e.clipboardData.getData("application/x-navi");
|
|
64605
|
+
let pasteValue;
|
|
64606
|
+
if (naviData) {
|
|
64607
|
+
try {
|
|
64608
|
+
pasteValue = JSON.parse(naviData);
|
|
64609
|
+
} catch {
|
|
64610
|
+
pasteValue = naviData;
|
|
64611
|
+
}
|
|
64612
|
+
} else {
|
|
64613
|
+
pasteValue = e.clipboardData.getData("text/plain");
|
|
64614
|
+
}
|
|
64615
|
+
dispatchRequestInteraction(pickerEl, {
|
|
64616
|
+
event: e,
|
|
64617
|
+
name: "paste",
|
|
64618
|
+
allowed: () => {
|
|
64619
|
+
dispatchRequestSetUIState(inputRef.current, pasteValue, {
|
|
64620
|
+
event: e
|
|
64621
|
+
});
|
|
64622
|
+
}
|
|
63884
64623
|
});
|
|
64624
|
+
e.preventDefault();
|
|
63885
64625
|
}
|
|
63886
|
-
})
|
|
63887
|
-
|
|
63888
|
-
|
|
63889
|
-
|
|
63890
|
-
|
|
63891
|
-
|
|
63892
|
-
|
|
63893
|
-
|
|
63894
|
-
|
|
63895
|
-
|
|
63896
|
-
|
|
63897
|
-
|
|
63898
|
-
|
|
63899
|
-
|
|
63900
|
-
|
|
63901
|
-
|
|
63902
|
-
|
|
63903
|
-
|
|
63904
|
-
|
|
63905
|
-
|
|
63906
|
-
|
|
63907
|
-
|
|
63908
|
-
|
|
63909
|
-
|
|
63910
|
-
|
|
63911
|
-
|
|
63912
|
-
|
|
63913
|
-
|
|
63914
|
-
|
|
63915
|
-
|
|
63916
|
-
|
|
63917
|
-
|
|
63918
|
-
|
|
63919
|
-
|
|
63920
|
-
|
|
63921
|
-
|
|
63922
|
-
|
|
63923
|
-
|
|
63924
|
-
|
|
63925
|
-
|
|
63926
|
-
|
|
63927
|
-
|
|
63928
|
-
|
|
63929
|
-
|
|
63930
|
-
|
|
63931
|
-
|
|
63932
|
-
|
|
63933
|
-
|
|
63934
|
-
|
|
63935
|
-
|
|
63936
|
-
|
|
63937
|
-
|
|
63938
|
-
|
|
63939
|
-
|
|
63940
|
-
|
|
63941
|
-
|
|
64626
|
+
}), variant === "icon" || variant === "headless" || ui === "default" ? null : jsx(Text, {
|
|
64627
|
+
className: "navi_picker_value",
|
|
64628
|
+
"navi-placeholder": uiStateHoldsNothing(value) ? "" : undefined,
|
|
64629
|
+
maxLines: maxLines,
|
|
64630
|
+
children: jsx(PickerOwnContent, {
|
|
64631
|
+
children: jsx(PickerContext.Provider, {
|
|
64632
|
+
value: {
|
|
64633
|
+
value,
|
|
64634
|
+
placeholder,
|
|
64635
|
+
maxLines
|
|
64636
|
+
},
|
|
64637
|
+
children: jsx(MaxLinesContext.Provider, {
|
|
64638
|
+
value: maxLines,
|
|
64639
|
+
children: ui === undefined ? jsx(PickerDefaultUI, {}) : ui
|
|
64640
|
+
})
|
|
64641
|
+
})
|
|
64642
|
+
})
|
|
64643
|
+
}), variant === "headless" || ui === "default" ? null : jsx("span", {
|
|
64644
|
+
className: "navi_picker_right_slot",
|
|
64645
|
+
children: jsx(PickerOwnContent, {
|
|
64646
|
+
children: clearable && interactive && value !== undefined && value !== "" ? jsx(Button, {
|
|
64647
|
+
command: "--navi-clear",
|
|
64648
|
+
commandFor: inputProps.id
|
|
64649
|
+
// The question, asked before the clear rather than by the
|
|
64650
|
+
// action the clear sends — see the --navi-clear command.
|
|
64651
|
+
,
|
|
64652
|
+
|
|
64653
|
+
confirm: clearConfirm,
|
|
64654
|
+
confirmPopupContent: clearConfirmPopupContent,
|
|
64655
|
+
tabIndex: "-1"
|
|
64656
|
+
// No navi-focus-delegate, unlike the identical button inside an
|
|
64657
|
+
// input: handing focus back to the picker's own input is what
|
|
64658
|
+
// opens the popup, and clearing is the opposite intention.
|
|
64659
|
+
,
|
|
64660
|
+
|
|
64661
|
+
icon: true,
|
|
64662
|
+
variant: "discrete"
|
|
64663
|
+
// What is busy once the clear is sent is the picker — the value
|
|
64664
|
+
// being removed is the whole field's, and the picker already
|
|
64665
|
+
// draws the wait around all of it. Two outlines for one wait is
|
|
64666
|
+
// one too many.
|
|
64667
|
+
,
|
|
64668
|
+
|
|
64669
|
+
loadingOutline: false
|
|
64670
|
+
// preventDefault, not just tabIndex="-1": a mousedown focuses
|
|
64671
|
+
// its target before any click happens, and this button should
|
|
64672
|
+
// never hold focus at all — the field keeps it.
|
|
64673
|
+
,
|
|
64674
|
+
|
|
64675
|
+
onMouseDown: e => {
|
|
64676
|
+
e.preventDefault();
|
|
64677
|
+
},
|
|
64678
|
+
flex: true,
|
|
64679
|
+
align: "center",
|
|
64680
|
+
children: jsx(Icon, {
|
|
64681
|
+
size: rightSlotIconSize,
|
|
64682
|
+
lineOverflow: "allow",
|
|
64683
|
+
children: jsx(CloseSvg, {})
|
|
64684
|
+
})
|
|
64685
|
+
}) : rightSlot === undefined ?
|
|
64686
|
+
// lineOverflow: what sits in the slot is an affordance, not a
|
|
64687
|
+
// character — a caller asking for a bigger one wants it bigger,
|
|
64688
|
+
// not capped at the height of the line it sits on
|
|
64689
|
+
jsx(Icon, {
|
|
63942
64690
|
size: rightSlotIconSize,
|
|
63943
64691
|
lineOverflow: "allow",
|
|
63944
|
-
children: jsx(
|
|
63945
|
-
})
|
|
63946
|
-
})
|
|
63947
|
-
|
|
63948
|
-
|
|
63949
|
-
|
|
63950
|
-
|
|
63951
|
-
|
|
63952
|
-
|
|
63953
|
-
children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
|
|
63954
|
-
}) : rightSlot
|
|
64692
|
+
children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
|
|
64693
|
+
}) : rightSlot
|
|
64694
|
+
})
|
|
64695
|
+
})]
|
|
64696
|
+
}), jsx(ControlFacadeChildrenWrapper, {
|
|
64697
|
+
...facadeChildrenProps,
|
|
64698
|
+
children: jsx("div", {
|
|
64699
|
+
className: "navi_picker_content",
|
|
64700
|
+
children: children
|
|
63955
64701
|
})
|
|
63956
|
-
})
|
|
63957
|
-
})]
|
|
63958
|
-
}), jsx(ControlFacadeChildrenWrapper, {
|
|
63959
|
-
...facadeChildrenProps,
|
|
63960
|
-
children: jsx(ReadOnlyContext.Provider, {
|
|
63961
|
-
value: readOnlyResolved,
|
|
63962
|
-
children: jsx("div", {
|
|
63963
|
-
className: "navi_picker_content",
|
|
63964
|
-
children: children
|
|
63965
|
-
})
|
|
64702
|
+
})]
|
|
63966
64703
|
})
|
|
63967
|
-
})
|
|
64704
|
+
})
|
|
64705
|
+
);
|
|
64706
|
+
};
|
|
64707
|
+
// What the picker draws itself — the value it shows, the furniture in its slot,
|
|
64708
|
+
// and whatever a caller puts in either — is not another control of the field
|
|
64709
|
+
// around it: none of it may take the id (nor the name) a <Field> hands down,
|
|
64710
|
+
// which is the picker's. Two controls under one id is one registry entry, and
|
|
64711
|
+
// the one that unmounts first — the clear cross the moment the field it emptied
|
|
64712
|
+
// is empty, a chip the moment its value is taken out — takes the picker's own
|
|
64713
|
+
// entry with it, leaving the picker looking for a controller that is gone.
|
|
64714
|
+
const PickerOwnContent = ({
|
|
64715
|
+
children
|
|
64716
|
+
}) => jsx(ControlIdContext.Provider, {
|
|
64717
|
+
value: undefined,
|
|
64718
|
+
children: jsx(ControlNameContext.Provider, {
|
|
64719
|
+
value: undefined,
|
|
64720
|
+
children: children
|
|
64721
|
+
})
|
|
64722
|
+
});
|
|
64723
|
+
|
|
64724
|
+
// `id` is what --navi-select/--navi-unselect carry (a list addresses its rows by
|
|
64725
|
+
// id); asked of a picker, what they carry is one entry of the list the picker
|
|
64726
|
+
// holds — the same thing a `<Picker.Chip value>` stands for.
|
|
64727
|
+
const requestPickerListEntry = (pickerEl, pickerInputEl, e, goal) => {
|
|
64728
|
+
const uiState = getUIStateFromElement(pickerInputEl);
|
|
64729
|
+
if (!Array.isArray(uiState)) {
|
|
64730
|
+
return;
|
|
64731
|
+
}
|
|
64732
|
+
const {
|
|
64733
|
+
id: entry
|
|
64734
|
+
} = e.detail;
|
|
64735
|
+
const isThere = uiState.some(item => compareTwoJsValues(item, entry));
|
|
64736
|
+
if (goal === "select" ? isThere : !isThere) {
|
|
64737
|
+
return;
|
|
64738
|
+
}
|
|
64739
|
+
const uiStateNext = goal === "select" ? [...uiState, entry] : uiState.filter(item => !compareTwoJsValues(item, entry));
|
|
64740
|
+
dispatchRequestInteraction(pickerEl, {
|
|
64741
|
+
event: e,
|
|
64742
|
+
name: goal,
|
|
64743
|
+
prevented: () => e.preventDefault(),
|
|
64744
|
+
allowed: () => {
|
|
64745
|
+
dispatchRequestSetUIState(pickerInputEl, uiStateNext, {
|
|
64746
|
+
event: e
|
|
64747
|
+
});
|
|
64748
|
+
}
|
|
63968
64749
|
});
|
|
63969
64750
|
};
|
|
63970
64751
|
const isWithinPickerContent = (el, pickerEl) => {
|
|
@@ -64109,6 +64890,7 @@ const PickerFirstResolver = props => {
|
|
|
64109
64890
|
});
|
|
64110
64891
|
};
|
|
64111
64892
|
const Picker = createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
|
|
64893
|
+
Picker.Chip = PickerChip;
|
|
64112
64894
|
Picker.UI = PickerDefaultUI;
|
|
64113
64895
|
Picker.UI.Date = PickerDateUI;
|
|
64114
64896
|
Picker.UI.Time = PickerTimeUI;
|