@jsenv/navi 0.29.102 → 0.29.104
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 +2003 -1015
- package/dist/jsenv_navi.js.map +215 -132
- package/dist/jsenv_navi_side_effects.js +32 -6
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/AI_INSTRUCTIONS.md +25 -4
- package/docs/badge_list.md +80 -0
- package/docs/control_value.md +50 -5
- package/docs/interactions.md +56 -8
- package/docs/typography.md +43 -2
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
6
6
|
export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
|
|
7
|
-
import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize,
|
|
7
|
+
import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, hasCSSSizeUnit, resolveOklchLightness, contrastColor, clickIsSuppressed, isTouchDrivenEvent, scrollIntoViewScoped, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, findBefore, findAfter, initFocusGroup, stringifyStyle as stringifyStyle$1, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
8
8
|
export { clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
9
9
|
import { signal, computed, effect, batch, untracked, useSignal } from "@preact/signals";
|
|
10
10
|
import { createContext, isValidElement, h, Fragment, render, toChildArray, options, cloneElement } from "preact";
|
|
@@ -38,7 +38,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
38
38
|
* any of these, and a number is the last resort, not the first tool.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
const css$
|
|
41
|
+
const css$14 = /* css */`
|
|
42
42
|
@layer navi {
|
|
43
43
|
:root {
|
|
44
44
|
/* A control that overlaps its neighbours (the members of a Group share
|
|
@@ -102,7 +102,7 @@ const css$15 = /* css */`
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
`;
|
|
105
|
-
import.meta.css = [css$
|
|
105
|
+
import.meta.css = [css$14, "@jsenv/navi/src/navi_z_indexes.js"];
|
|
106
106
|
|
|
107
107
|
const addIntoArray = (array, ...valuesToAdd) => {
|
|
108
108
|
if (valuesToAdd.length === 1) {
|
|
@@ -371,7 +371,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
371
371
|
* the very first render and the browser does everything on its own.
|
|
372
372
|
*/
|
|
373
373
|
const URL_TARGET_ATTRIBUTE = "data-url-target";
|
|
374
|
-
const css$
|
|
374
|
+
const css$13 = /* css */`
|
|
375
375
|
@layer navi {
|
|
376
376
|
[${URL_TARGET_ATTRIBUTE}] {
|
|
377
377
|
animation: navi_url_target var(--navi-url-target-duration, 2000ms)
|
|
@@ -389,7 +389,7 @@ const css$14 = /* css */`
|
|
|
389
389
|
}
|
|
390
390
|
}
|
|
391
391
|
`;
|
|
392
|
-
import.meta.css = [css$
|
|
392
|
+
import.meta.css = [css$13, "@jsenv/navi/src/nav/url_target/url_target.js"];
|
|
393
393
|
let urlTargetOptions = {
|
|
394
394
|
block: "start",
|
|
395
395
|
behavior: "instant",
|
|
@@ -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,302 @@ 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, and the question that settles it is: **does
|
|
2339
|
+
* this affordance write to the control it sits in?**
|
|
2340
|
+
*
|
|
2341
|
+
* - it does, and once the value cannot be changed it has nothing left to offer:
|
|
2342
|
+
* it GOES. A remove cross that still removes is worse than no cross, and one
|
|
2343
|
+
* that refuses politely still says "there is something to remove here" on a
|
|
2344
|
+
* row that is only being read. That is the default.
|
|
2345
|
+
* - it does, but its presence is information in itself: `"refuse"` keeps it and
|
|
2346
|
+
* refuses in its own words, like every other navi control.
|
|
2347
|
+
* - it does NOT — a diskette saving a row into the reader's own address book, a
|
|
2348
|
+
* badge explaining why a placement is odd. The read-only around it is about a
|
|
2349
|
+
* value it never touches, so `"always"` ignores it: the affordance stays lit
|
|
2350
|
+
* and stays pressable. It is on the caller to use it only for a gesture that
|
|
2351
|
+
* genuinely writes nothing to the control around it.
|
|
2352
|
+
*
|
|
2353
|
+
* Whichever mode, the affordance's own handler runs from inside its own gate
|
|
2354
|
+
* rather than from the DOM: a caller's `onClick` fires before any of this, and
|
|
2355
|
+
* would go off from a button drawn greyed.
|
|
2356
|
+
*/
|
|
2357
|
+
|
|
2358
|
+
|
|
2359
|
+
/**
|
|
2360
|
+
* The whole claim, in the DOM, on any element — a `<button>` an application
|
|
2361
|
+
* draws itself as much as a navi control. It is read from the outside and
|
|
2362
|
+
* nowhere else: by the controls above (below), and by the gesture readers
|
|
2363
|
+
* (@jsenv/dom's DRAG_EXCLUDED_SELECTOR and DRAG_IGNORED_SELECTOR), which is why
|
|
2364
|
+
* one attribute is enough and the `ownTarget` prop only writes it.
|
|
2365
|
+
*
|
|
2366
|
+
* Its value is the mode, when there is one to say: `data-own-target="always"`.
|
|
2367
|
+
*/
|
|
2368
|
+
const OWN_TARGET_ATTRIBUTE = "data-own-target";
|
|
2369
|
+
|
|
2370
|
+
/**
|
|
2371
|
+
* Whether `event` was aimed at an own target sitting below this control — in
|
|
2372
|
+
* which case the control is not what the press was for and must not answer it.
|
|
2373
|
+
*
|
|
2374
|
+
* Read from the event's target rather than from a mark left by a handler: the
|
|
2375
|
+
* question is "who is this press for", and the DOM between the pointer and the
|
|
2376
|
+
* control is the whole answer. Nothing is asked of the own target itself, which
|
|
2377
|
+
* is what lets it be anything — a button, a link, a field, a bare element.
|
|
2378
|
+
*/
|
|
2379
|
+
const isAimedAtOwnTargetBelow = (event, controlHost) => {
|
|
2380
|
+
const target = event?.target;
|
|
2381
|
+
if (!target || typeof target.closest !== "function") {
|
|
2382
|
+
return false;
|
|
2383
|
+
}
|
|
2384
|
+
const ownTarget = target.closest(`[${OWN_TARGET_ATTRIBUTE}]`);
|
|
2385
|
+
if (!ownTarget) {
|
|
2386
|
+
return false;
|
|
2387
|
+
}
|
|
2388
|
+
// The claim is against what is ABOVE it: the control that IS the own target,
|
|
2389
|
+
// and any control living inside it, are being aimed at like anything else.
|
|
2390
|
+
if (ownTarget === controlHost || ownTarget.contains(controlHost)) {
|
|
2391
|
+
return false;
|
|
2392
|
+
}
|
|
2393
|
+
// From the root rather than the host: a layered control (a picker holding an
|
|
2394
|
+
// input) has its gate on the input, and what the application drew sits beside
|
|
2395
|
+
// it, not in it.
|
|
2396
|
+
const controlRoot = controlHost.closest("[navi-control]") || controlHost;
|
|
2397
|
+
return controlRoot.contains(ownTarget);
|
|
2398
|
+
};
|
|
2399
|
+
|
|
2400
|
+
/**
|
|
2401
|
+
* Whether the read-only, disabled and busy of the zone around this own target
|
|
2402
|
+
* are about it at all — see the modes at the top of this file.
|
|
2403
|
+
*/
|
|
2404
|
+
const ownTargetIgnoresZoneState = (ownTarget) => ownTarget === "always";
|
|
2405
|
+
|
|
2406
|
+
/**
|
|
2407
|
+
* Whether an own target has nothing to offer where it sits: it writes to the
|
|
2408
|
+
* control around it, and that control cannot be changed, so the affordance goes.
|
|
2409
|
+
*/
|
|
2410
|
+
const useOwnTargetHidden = (props) => {
|
|
2411
|
+
const disabled = useContext(DisabledContext);
|
|
2412
|
+
const readOnly = useContext(ReadOnlyContext);
|
|
2413
|
+
const loading = useContext(LoadingContext$1);
|
|
2414
|
+
const { ownTarget } = props;
|
|
2415
|
+
if (
|
|
2416
|
+
!ownTarget ||
|
|
2417
|
+
ownTarget === "refuse" ||
|
|
2418
|
+
ownTargetIgnoresZoneState(ownTarget)
|
|
2419
|
+
) {
|
|
2420
|
+
return false;
|
|
2421
|
+
}
|
|
2422
|
+
return Boolean(disabled || readOnly || loading);
|
|
2423
|
+
};
|
|
2424
|
+
|
|
2117
2425
|
/*
|
|
2118
2426
|
* Deep structural equality for arbitrary JS values — what `===` can't do but this
|
|
2119
2427
|
* codebase constantly needs: memoization cache keys ({ id: 1 } equal to { id: 1 }),
|
|
@@ -6490,83 +6798,6 @@ const isInertOnClick = (element) => {
|
|
|
6490
6798
|
return true;
|
|
6491
6799
|
};
|
|
6492
6800
|
|
|
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
6801
|
/**
|
|
6571
6802
|
* DOM utilities for the proxy control pattern.
|
|
6572
6803
|
*
|
|
@@ -6656,6 +6887,10 @@ const findControlProxy = (el) => {
|
|
|
6656
6887
|
return firstProxy;
|
|
6657
6888
|
};
|
|
6658
6889
|
|
|
6890
|
+
let renderMessageText = text => text;
|
|
6891
|
+
const setCalloutMessageTextRenderer = renderer => {
|
|
6892
|
+
renderMessageText = renderer;
|
|
6893
|
+
};
|
|
6659
6894
|
const CalloutRequestCloseContext = createContext();
|
|
6660
6895
|
const useCalloutRequestClose = () => {
|
|
6661
6896
|
return useContext(CalloutRequestCloseContext);
|
|
@@ -6670,6 +6905,48 @@ const renderIntoCallout = (jsx$1, calloutMessageElement, {
|
|
|
6670
6905
|
render(calloutJsx, calloutMessageElement);
|
|
6671
6906
|
};
|
|
6672
6907
|
|
|
6908
|
+
// An HTML message is rendered through preact rather than innerHTML so that its
|
|
6909
|
+
// text can go through renderMessageText: an emoji in a validation message must
|
|
6910
|
+
// not make the first line taller than the icon and close button beside it.
|
|
6911
|
+
const renderHtmlIntoCallout = (html, calloutMessageElement, {
|
|
6912
|
+
requestClose
|
|
6913
|
+
}) => {
|
|
6914
|
+
const template = document.createElement("template");
|
|
6915
|
+
template.innerHTML = html;
|
|
6916
|
+
renderIntoCallout(domToVNodes(template.content), calloutMessageElement, {
|
|
6917
|
+
requestClose
|
|
6918
|
+
});
|
|
6919
|
+
};
|
|
6920
|
+
|
|
6921
|
+
// Unmounts whatever preact rendered before the element is filled by hand
|
|
6922
|
+
// (a DOM node, an iframe); innerHTML alone would leave preact believing its
|
|
6923
|
+
// tree is still there.
|
|
6924
|
+
const clearCalloutMessage = calloutMessageElement => {
|
|
6925
|
+
render(null, calloutMessageElement);
|
|
6926
|
+
calloutMessageElement.innerHTML = "";
|
|
6927
|
+
};
|
|
6928
|
+
const domToVNodes = node => {
|
|
6929
|
+
const vnodes = [];
|
|
6930
|
+
for (const child of node.childNodes) {
|
|
6931
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
6932
|
+
vnodes.push(renderMessageText(child.data));
|
|
6933
|
+
continue;
|
|
6934
|
+
}
|
|
6935
|
+
if (child.nodeType !== Node.ELEMENT_NODE) {
|
|
6936
|
+
continue;
|
|
6937
|
+
}
|
|
6938
|
+
const props = {};
|
|
6939
|
+
for (const {
|
|
6940
|
+
name,
|
|
6941
|
+
value
|
|
6942
|
+
} of child.attributes) {
|
|
6943
|
+
props[name] = value;
|
|
6944
|
+
}
|
|
6945
|
+
vnodes.push(h(child.localName, props, ...domToVNodes(child)));
|
|
6946
|
+
}
|
|
6947
|
+
return vnodes;
|
|
6948
|
+
};
|
|
6949
|
+
|
|
6673
6950
|
installImportMetaCssBuild(import.meta);/**
|
|
6674
6951
|
* A callout component that mimics native browser validation messages.
|
|
6675
6952
|
* Features:
|
|
@@ -6679,7 +6956,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
6679
6956
|
* - Arrow automatically shows when pointing at a valid anchor element
|
|
6680
6957
|
* - Centers in viewport when no anchor element provided or anchor is too big
|
|
6681
6958
|
*/
|
|
6682
|
-
const css$
|
|
6959
|
+
const css$12 = /* css */`
|
|
6683
6960
|
@layer navi {
|
|
6684
6961
|
.navi_callout {
|
|
6685
6962
|
/* A callout is parented to what it explains, so it inherits from it — and
|
|
@@ -6781,7 +7058,10 @@ const css$13 = /* css */`
|
|
|
6781
7058
|
}
|
|
6782
7059
|
|
|
6783
7060
|
.navi_callout_body {
|
|
6784
|
-
|
|
7061
|
+
/* The first line of the message: the icon and the close button sit
|
|
7062
|
+
in columns of that height, pinned to the top, so both stay level
|
|
7063
|
+
with it however many lines the message takes. */
|
|
7064
|
+
--callout-icon-height: round(1lh, 1px);
|
|
6785
7065
|
|
|
6786
7066
|
position: relative;
|
|
6787
7067
|
display: flex;
|
|
@@ -6837,10 +7117,15 @@ const css$13 = /* css */`
|
|
|
6837
7117
|
align-self: flex-start;
|
|
6838
7118
|
|
|
6839
7119
|
.navi_callout_close_button {
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
7120
|
+
/* A square filling the column, so the whole first line is the target;
|
|
7121
|
+
the padding keeps the cross itself at glyph size. */
|
|
7122
|
+
display: inline-flex;
|
|
7123
|
+
box-sizing: border-box;
|
|
7124
|
+
aspect-ratio: 1 / 1;
|
|
7125
|
+
height: 100%;
|
|
7126
|
+
padding: 0.2em;
|
|
7127
|
+
align-items: center;
|
|
7128
|
+
justify-content: center;
|
|
6844
7129
|
color: currentColor;
|
|
6845
7130
|
font-size: inherit;
|
|
6846
7131
|
background: none;
|
|
@@ -6922,7 +7207,7 @@ const openCallout = (message, {
|
|
|
6922
7207
|
skipFocus = false,
|
|
6923
7208
|
debug = () => {}
|
|
6924
7209
|
} = {}) => {
|
|
6925
|
-
import.meta.css = [css$
|
|
7210
|
+
import.meta.css = [css$12, "@jsenv/navi/src/control/rules/callout/callout.js"];
|
|
6926
7211
|
if (debug === true) {
|
|
6927
7212
|
debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
|
|
6928
7213
|
}
|
|
@@ -7104,11 +7389,11 @@ const openCallout = (message, {
|
|
|
7104
7389
|
} else if (newMessage instanceof Node) {
|
|
7105
7390
|
// Handle DOM node (cloned from CSS selector)
|
|
7106
7391
|
debug(`callout update message (node)`);
|
|
7107
|
-
calloutMessageElement
|
|
7392
|
+
clearCalloutMessage(calloutMessageElement);
|
|
7108
7393
|
calloutMessageElement.appendChild(newMessage);
|
|
7109
7394
|
} else if (typeof newMessage === "function") {
|
|
7110
7395
|
debug(`callout update message (function)`);
|
|
7111
|
-
calloutMessageElement
|
|
7396
|
+
clearCalloutMessage(calloutMessageElement);
|
|
7112
7397
|
newMessage({
|
|
7113
7398
|
renderIntoCallout: jsx => renderIntoCallout(jsx, calloutMessageElement, {
|
|
7114
7399
|
requestClose
|
|
@@ -7133,12 +7418,13 @@ const openCallout = (message, {
|
|
|
7133
7418
|
iframe.style.backgroundColor = "white";
|
|
7134
7419
|
iframe.srcdoc = newMessage;
|
|
7135
7420
|
debug(`callout update message (html document iframe)`);
|
|
7136
|
-
|
|
7137
|
-
calloutMessageElement.innerHTML = "";
|
|
7421
|
+
clearCalloutMessage(calloutMessageElement);
|
|
7138
7422
|
calloutMessageElement.appendChild(iframe);
|
|
7139
7423
|
} else {
|
|
7140
7424
|
debug(`callout update message: ${typeof newMessage === "string" ? newMessage.slice(0, 80) : String(newMessage)}`);
|
|
7141
|
-
calloutMessageElement
|
|
7425
|
+
renderHtmlIntoCallout(String(newMessage), calloutMessageElement, {
|
|
7426
|
+
requestClose
|
|
7427
|
+
});
|
|
7142
7428
|
}
|
|
7143
7429
|
}
|
|
7144
7430
|
// After updating content the callout size likely changed — re-position immediately
|
|
@@ -8241,8 +8527,6 @@ const ABORTED = { id: "aborted" };
|
|
|
8241
8527
|
const FAILED = { id: "failed" };
|
|
8242
8528
|
const COMPLETED = { id: "completed" };
|
|
8243
8529
|
|
|
8244
|
-
const CONSTRAINT_ATTRIBUTE_SET = new Set();
|
|
8245
|
-
|
|
8246
8530
|
const BUSY_CONSTRAINT = {
|
|
8247
8531
|
name: "busy",
|
|
8248
8532
|
messageAttribute: "data-busy-message",
|
|
@@ -10295,6 +10579,20 @@ const onRequestInteraction = (
|
|
|
10295
10579
|
|
|
10296
10580
|
const currentTarget = requestInteractionCustomEvent.currentTarget;
|
|
10297
10581
|
const controlHost = findControlHost(currentTarget) || currentTarget;
|
|
10582
|
+
|
|
10583
|
+
// Aimed at something else that lives in this control's box: a chip's cross, an
|
|
10584
|
+
// eye, a diskette. The press is that affordance's alone (see own_target.js),
|
|
10585
|
+
// and stepping back here — rather than stopping the propagation over there —
|
|
10586
|
+
// is what leaves the event whole for everything that is not a navi
|
|
10587
|
+
// interaction. Stepping back, not refusing: the reaction never happened, so
|
|
10588
|
+
// its `prevented`/`always` (an `e.preventDefault()`, for most of them) have
|
|
10589
|
+
// nothing to undo and would take the press from the affordance itself.
|
|
10590
|
+
if (isAimedAtOwnTargetBelow(event, controlHost)) {
|
|
10591
|
+
debugInteraction(event, `"${name}" is for an own target below`);
|
|
10592
|
+
requestInteractionCustomEvent.preventDefault();
|
|
10593
|
+
return false;
|
|
10594
|
+
}
|
|
10595
|
+
|
|
10298
10596
|
const controller = controlHost.__uiStateController__;
|
|
10299
10597
|
|
|
10300
10598
|
if (controller && !bypassInteractivity) {
|
|
@@ -16085,6 +16383,26 @@ const POSITION_PROPS = {
|
|
|
16085
16383
|
return { transform: `skew(${value})` };
|
|
16086
16384
|
},
|
|
16087
16385
|
};
|
|
16386
|
+
const singleLineEllipsisStyles = () => {
|
|
16387
|
+
return {
|
|
16388
|
+
overflow: "hidden",
|
|
16389
|
+
textOverflow: "ellipsis",
|
|
16390
|
+
overflowWrap: "normal",
|
|
16391
|
+
};
|
|
16392
|
+
};
|
|
16393
|
+
// The lines beyond the clamp are still laid out, so the clip edge decides
|
|
16394
|
+
// whether the top of the first hidden one is visible: "overflow: hidden" clips
|
|
16395
|
+
// at the padding box and lets it show inside the block-end padding. Clipping at
|
|
16396
|
+
// the content box instead ends the element right after its last visible line.
|
|
16397
|
+
const lineClampStyles = (value) => {
|
|
16398
|
+
return {
|
|
16399
|
+
"overflow": "clip",
|
|
16400
|
+
"overflowClipMargin": "content-box",
|
|
16401
|
+
"display": "-webkit-box",
|
|
16402
|
+
"-webkit-box-orient": "vertical",
|
|
16403
|
+
"-webkit-line-clamp": value,
|
|
16404
|
+
};
|
|
16405
|
+
};
|
|
16088
16406
|
const TYPO_PROPS = {
|
|
16089
16407
|
font: applyOnCSSProp("fontFamily"),
|
|
16090
16408
|
fontFamily: PASS_THROUGH,
|
|
@@ -16122,39 +16440,21 @@ const TYPO_PROPS = {
|
|
|
16122
16440
|
return null;
|
|
16123
16441
|
}
|
|
16124
16442
|
if (value === 1 || value === "1") {
|
|
16125
|
-
return
|
|
16126
|
-
overflow: "hidden",
|
|
16127
|
-
textOverflow: "ellipsis",
|
|
16128
|
-
overflowWrap: "normal",
|
|
16129
|
-
};
|
|
16443
|
+
return singleLineEllipsisStyles();
|
|
16130
16444
|
}
|
|
16131
|
-
return
|
|
16132
|
-
"overflow": "hidden",
|
|
16133
|
-
"display": "-webkit-box",
|
|
16134
|
-
"-webkit-box-orient": "vertical",
|
|
16135
|
-
"-webkit-line-clamp": value,
|
|
16136
|
-
};
|
|
16445
|
+
return lineClampStyles(value);
|
|
16137
16446
|
},
|
|
16138
16447
|
overflowEllipsis: (value) => {
|
|
16139
16448
|
if (!value) {
|
|
16140
16449
|
return null;
|
|
16141
16450
|
}
|
|
16142
|
-
return
|
|
16143
|
-
overflow: "hidden",
|
|
16144
|
-
textOverflow: "ellipsis",
|
|
16145
|
-
overflowWrap: "normal",
|
|
16146
|
-
};
|
|
16451
|
+
return singleLineEllipsisStyles();
|
|
16147
16452
|
},
|
|
16148
16453
|
lineClamp: (value) => {
|
|
16149
16454
|
if (!value) {
|
|
16150
16455
|
return null;
|
|
16151
16456
|
}
|
|
16152
|
-
return
|
|
16153
|
-
"overflow": "hidden",
|
|
16154
|
-
"display": "-webkit-box",
|
|
16155
|
-
"-webkit-box-orient": "vertical",
|
|
16156
|
-
"-webkit-line-clamp": value,
|
|
16157
|
-
};
|
|
16457
|
+
return lineClampStyles(value);
|
|
16158
16458
|
},
|
|
16159
16459
|
textAlign: PASS_THROUGH,
|
|
16160
16460
|
textBox: PASS_THROUGH,
|
|
@@ -16200,6 +16500,7 @@ const VISUAL_PROPS = {
|
|
|
16200
16500
|
overflow: PASS_THROUGH,
|
|
16201
16501
|
overflowX: PASS_THROUGH,
|
|
16202
16502
|
overflowY: PASS_THROUGH,
|
|
16503
|
+
overflowClipMargin: PASS_THROUGH,
|
|
16203
16504
|
objectFit: PASS_THROUGH,
|
|
16204
16505
|
accentColor: PASS_THROUGH,
|
|
16205
16506
|
scrollbarWidth: PASS_THROUGH,
|
|
@@ -19199,7 +19500,7 @@ const setupNetworkMonitoring = () => {
|
|
|
19199
19500
|
};
|
|
19200
19501
|
setupNetworkMonitoring();
|
|
19201
19502
|
|
|
19202
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
19503
|
+
installImportMetaCssBuild(import.meta);const css$11 = /* css */`
|
|
19203
19504
|
.navi_loading_indicator_fluid_container {
|
|
19204
19505
|
position: relative;
|
|
19205
19506
|
display: flex;
|
|
@@ -19231,7 +19532,7 @@ const LoadingIndicatorFluid = ({
|
|
|
19231
19532
|
visuallyHidden,
|
|
19232
19533
|
...rest
|
|
19233
19534
|
}) => {
|
|
19234
|
-
import.meta.css = [css$
|
|
19535
|
+
import.meta.css = [css$11, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
|
|
19235
19536
|
const ref = useRef(null);
|
|
19236
19537
|
// The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
|
|
19237
19538
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
@@ -19436,7 +19737,7 @@ const LoadingRectangleSvg = ({
|
|
|
19436
19737
|
});
|
|
19437
19738
|
};
|
|
19438
19739
|
|
|
19439
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
19740
|
+
installImportMetaCssBuild(import.meta);const css$10 = /* css */`
|
|
19440
19741
|
.navi_loading_outline_wrapper {
|
|
19441
19742
|
position: absolute;
|
|
19442
19743
|
/* Controls place the outline slightly outside their box, right on top of
|
|
@@ -19473,7 +19774,7 @@ installImportMetaCssBuild(import.meta);const css$11 = /* css */`
|
|
|
19473
19774
|
}
|
|
19474
19775
|
`;
|
|
19475
19776
|
const LoadingOutline = props => {
|
|
19476
|
-
import.meta.css = [css$
|
|
19777
|
+
import.meta.css = [css$10, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
|
|
19477
19778
|
if (props.containerRef) {
|
|
19478
19779
|
const container = props.containerRef.current;
|
|
19479
19780
|
if (!container) {
|
|
@@ -19663,6 +19964,256 @@ const getHrefTargetInfo = (href) => {
|
|
|
19663
19964
|
};
|
|
19664
19965
|
};
|
|
19665
19966
|
|
|
19967
|
+
/**
|
|
19968
|
+
* A variant of useLayoutEffect that accounts for ancestor <dialog>/<details>
|
|
19969
|
+
* or popover visibility.
|
|
19970
|
+
*
|
|
19971
|
+
* Motivation: some effects (auto-scroll, measurement, focus) only make sense
|
|
19972
|
+
* when the element is actually presented on screen. A plain useLayoutEffect
|
|
19973
|
+
* fires on mount even when the component is inside a closed <dialog>, a
|
|
19974
|
+
* collapsed <details>, or a hidden popover, where scroll and layout operations
|
|
19975
|
+
* are no-ops.
|
|
19976
|
+
*
|
|
19977
|
+
* Behavior:
|
|
19978
|
+
* - No <dialog>/<details>/[popover] ancestor → runs like a normal
|
|
19979
|
+
* useLayoutEffect with the provided deps.
|
|
19980
|
+
* - Inside a closed/hidden ancestor → skips the initial run; instead runs
|
|
19981
|
+
* the callback once the ancestor opens — see @jsenv/dom's own
|
|
19982
|
+
* observeAncestorOpenState for exactly how that's detected, and why it
|
|
19983
|
+
* matters that it happens before the browser paints.
|
|
19984
|
+
* - Inside an open ancestor → runs on mount AND every subsequent open.
|
|
19985
|
+
*
|
|
19986
|
+
* The callback's second argument is always a `navi_displayed` CustomEvent,
|
|
19987
|
+
* with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
|
|
19988
|
+
* - No <dialog>/<details>/[popover]/[aria-expanded] ancestor at all →
|
|
19989
|
+
* `{ ancestor: document, ancestorType: "document" }`.
|
|
19990
|
+
* - Otherwise → `{ ancestor: <the matched element>, ancestorType: "dialog"
|
|
19991
|
+
* | "popover" | "details" | "aria-expanded" }`.
|
|
19992
|
+
* `becauseAncestorOpened` distinguishes the two ways of coming on screen:
|
|
19993
|
+
* - true — the element was already mounted and the ancestor just opened,
|
|
19994
|
+
* revealing it along with everything else it holds. The opening has an
|
|
19995
|
+
* owner (the ancestor's own transferFocus/openEffect), and what it reveals
|
|
19996
|
+
* should defer to it — see use_auto_focus.js.
|
|
19997
|
+
* - false — the element was mounted just now, into a surface already on
|
|
19998
|
+
* screen (or into the plain document). Nothing else owns this appearance:
|
|
19999
|
+
* what the element says about itself (an autofocus, a measurement) is the
|
|
20000
|
+
* only word there is.
|
|
20001
|
+
*
|
|
20002
|
+
* Usage:
|
|
20003
|
+
* useDisplayedLayoutEffect(ref, () => {
|
|
20004
|
+
* scrollToSelected();
|
|
20005
|
+
* }, []);
|
|
20006
|
+
*/
|
|
20007
|
+
const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
20008
|
+
if (typeof callback !== "function") {
|
|
20009
|
+
throw new TypeError("useDisplayedLayoutEffect: callback is not a function");
|
|
20010
|
+
}
|
|
20011
|
+
|
|
20012
|
+
// Keep a stable ref so the open listener always calls the latest callback
|
|
20013
|
+
// without needing to be re-registered when deps change.
|
|
20014
|
+
const callbackRef = useRef(callback);
|
|
20015
|
+
callbackRef.current = callback;
|
|
20016
|
+
|
|
20017
|
+
// Run on mount (or when deps change) — but only if the element is visible.
|
|
20018
|
+
useLayoutEffect(() => {
|
|
20019
|
+
const el = ref.current;
|
|
20020
|
+
if (!el) {
|
|
20021
|
+
return;
|
|
20022
|
+
}
|
|
20023
|
+
const ancestor = closestOpenableAncestor(el);
|
|
20024
|
+
if (!ancestor) {
|
|
20025
|
+
callbackRef.current(el, createDisplayedEvent(document, false));
|
|
20026
|
+
return;
|
|
20027
|
+
}
|
|
20028
|
+
if (!isAncestorOpen(ancestor)) {
|
|
20029
|
+
// Ancestor is closed — skip now; the observeAncestorOpenState call
|
|
20030
|
+
// below will fire once it opens.
|
|
20031
|
+
return;
|
|
20032
|
+
}
|
|
20033
|
+
callbackRef.current(el, createDisplayedEvent(ancestor, false));
|
|
20034
|
+
}, deps);
|
|
20035
|
+
|
|
20036
|
+
// Re-run every time the ancestor opens.
|
|
20037
|
+
useLayoutEffect(() => {
|
|
20038
|
+
const el = ref.current;
|
|
20039
|
+
if (!el) {
|
|
20040
|
+
return undefined;
|
|
20041
|
+
}
|
|
20042
|
+
const ancestor = closestOpenableAncestor(el);
|
|
20043
|
+
if (!ancestor) {
|
|
20044
|
+
return undefined;
|
|
20045
|
+
}
|
|
20046
|
+
return observeAncestorOpenState(ancestor, ({ isOpen }) => {
|
|
20047
|
+
if (!isOpen) {
|
|
20048
|
+
return;
|
|
20049
|
+
}
|
|
20050
|
+
const lastEl = ref.current;
|
|
20051
|
+
callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
|
|
20052
|
+
});
|
|
20053
|
+
}, []);
|
|
20054
|
+
};
|
|
20055
|
+
|
|
20056
|
+
const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
|
|
20057
|
+
return new CustomEvent("navi_displayed", {
|
|
20058
|
+
detail: {
|
|
20059
|
+
ancestor,
|
|
20060
|
+
ancestorType: getAncestorOpenType(ancestor),
|
|
20061
|
+
becauseAncestorOpened,
|
|
20062
|
+
},
|
|
20063
|
+
});
|
|
20064
|
+
};
|
|
20065
|
+
|
|
20066
|
+
installImportMetaCssBuild(import.meta);// # TextAnchor — how it works
|
|
20067
|
+
const css$$ = /* css */`
|
|
20068
|
+
.navi_text_anchor {
|
|
20069
|
+
vertical-align: baseline;
|
|
20070
|
+
user-select: none;
|
|
20071
|
+
overflow: hidden;
|
|
20072
|
+
}
|
|
20073
|
+
`;
|
|
20074
|
+
|
|
20075
|
+
/**
|
|
20076
|
+
* Positions children vertically relative to the surrounding text, correcting for font-size differences.
|
|
20077
|
+
*
|
|
20078
|
+
* Place this component around any inline element whose font-size differs from the surrounding text.
|
|
20079
|
+
* It renders an invisible anchor that inherits the surrounding text's font metrics, then shifts
|
|
20080
|
+
* the child so that its visual position matches the requested `textAnchor` value — regardless of
|
|
20081
|
+
* font-size, display type (inline, inline-block, inline-flex…), or the active `vertical-align`.
|
|
20082
|
+
*
|
|
20083
|
+
* @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [textAnchor="char-bottom"]
|
|
20084
|
+
* - `"line-top"` — child top aligns with the top of the surrounding line box
|
|
20085
|
+
* - `"char-top"` — child top aligns with the top of visible characters (ink ascent)
|
|
20086
|
+
* - `"center"` — child is vertically centered on the surrounding line box
|
|
20087
|
+
* - `"char-bottom"` — child bottom aligns to the text baseline (no correction, browser default)
|
|
20088
|
+
* - `"line-bottom"` — child bottom aligns with the bottom of the surrounding line box
|
|
20089
|
+
* @param {{ size?: number, verticalAlign?: string }} [lineLayout]
|
|
20090
|
+
* Describes the surrounding line context. Used as layout-effect dependencies so the correction
|
|
20091
|
+
* reruns when the surrounding text's font-size or vertical-align changes.
|
|
20092
|
+
* @param {import("ignore:preact").RefObject} childRef — ref on the child element to reposition
|
|
20093
|
+
*/
|
|
20094
|
+
const TextAnchor = ({
|
|
20095
|
+
childRef,
|
|
20096
|
+
children,
|
|
20097
|
+
textAnchor = "char-bottom",
|
|
20098
|
+
textKey,
|
|
20099
|
+
textSize,
|
|
20100
|
+
lineLayout
|
|
20101
|
+
}) => {
|
|
20102
|
+
import.meta.css = [css$$, "@jsenv/navi/src/text/text_anchor.jsx"];
|
|
20103
|
+
const anchorRef = useRef();
|
|
20104
|
+
|
|
20105
|
+
// Plain useLayoutEffect would also fire while an ancestor dialog/popover
|
|
20106
|
+
// (e.g. a closed SidePanel) is still display:none — every rect involved
|
|
20107
|
+
// reads 0×0 at that point, so the math trivially (and wrongly) resolves
|
|
20108
|
+
// to topOffset 0: not a real "no correction needed" result, just a
|
|
20109
|
+
// zero-by-zero coincidence that happens to look fine only because it
|
|
20110
|
+
// leaves the browser's own default alignment untouched. The real
|
|
20111
|
+
// correction then only gets applied later, on whatever unrelated
|
|
20112
|
+
// re-render next happens to change one of this effect's own deps —
|
|
20113
|
+
// which reads as the child "jumping" even though nothing about its own
|
|
20114
|
+
// geometry changed. useDisplayedLayoutEffect skips the initial run in
|
|
20115
|
+
// that case and reruns once the ancestor actually opens instead.
|
|
20116
|
+
useDisplayedLayoutEffect(anchorRef, anchorEl => {
|
|
20117
|
+
const childEl = childRef.current;
|
|
20118
|
+
if (!anchorEl || !childEl) {
|
|
20119
|
+
return;
|
|
20120
|
+
}
|
|
20121
|
+
// Only correct when the anchor lives in an inline formatting context.
|
|
20122
|
+
// If the parent is a flex/grid container, inline layout rules don't apply
|
|
20123
|
+
// and our font-metrics model is invalid.
|
|
20124
|
+
const parentDisplay = getComputedStyle(anchorEl.parentElement).display;
|
|
20125
|
+
if (parentDisplay !== "inline" && parentDisplay !== "inline-block" && parentDisplay !== "block") {
|
|
20126
|
+
// we must hide the anchor otherwise it would affect layout without providing any benefit (would trigger flex gap for instance)
|
|
20127
|
+
anchorEl.setAttribute("hidden", "");
|
|
20128
|
+
setTopOffset(childEl, 0);
|
|
20129
|
+
return;
|
|
20130
|
+
}
|
|
20131
|
+
anchorEl.removeAttribute("hidden");
|
|
20132
|
+
const topOffset = computeTopOffset({
|
|
20133
|
+
anchorEl,
|
|
20134
|
+
childEl,
|
|
20135
|
+
textAnchor
|
|
20136
|
+
});
|
|
20137
|
+
setTopOffset(childEl, topOffset);
|
|
20138
|
+
}, [textAnchor, textKey, textSize, lineLayout?.size, lineLayout?.verticalAlign]);
|
|
20139
|
+
return jsxs(Fragment$1, {
|
|
20140
|
+
children: [children, jsx("span", {
|
|
20141
|
+
ref: anchorRef,
|
|
20142
|
+
className: "navi_text_anchor",
|
|
20143
|
+
"aria-hidden": "true",
|
|
20144
|
+
children: "\u200B"
|
|
20145
|
+
})]
|
|
20146
|
+
});
|
|
20147
|
+
};
|
|
20148
|
+
const setTopOffset = (childEl, topOffset) => {
|
|
20149
|
+
// position:relative + top shifts the element visually.
|
|
20150
|
+
// marginTop: -topOffset makes the layout box follow the visual position, so any container
|
|
20151
|
+
// (button, link, box…) computes its own padding/border/height based on the real final position
|
|
20152
|
+
// rather than the original unshifted one. This means a badge inside a button will symmetrically
|
|
20153
|
+
// expand the button height instead of overflowing or being clipped.
|
|
20154
|
+
// marginBottom: topOffset compensates the marginTop so the line height stays unchanged —
|
|
20155
|
+
// the shift is purely a repositioning, not an inflation of the line.
|
|
20156
|
+
if (!topOffset) {
|
|
20157
|
+
childEl.style.position = "";
|
|
20158
|
+
childEl.style.top = "";
|
|
20159
|
+
childEl.style.marginTop = "";
|
|
20160
|
+
childEl.style.marginBottom = "";
|
|
20161
|
+
return;
|
|
20162
|
+
}
|
|
20163
|
+
childEl.style.position = "relative";
|
|
20164
|
+
childEl.style.top = `${topOffset}px`;
|
|
20165
|
+
childEl.style.marginTop = `${-topOffset}px`;
|
|
20166
|
+
childEl.style.marginBottom = `${topOffset}px`;
|
|
20167
|
+
};
|
|
20168
|
+
const computeTopOffset = ({
|
|
20169
|
+
anchorEl,
|
|
20170
|
+
childEl,
|
|
20171
|
+
textAnchor
|
|
20172
|
+
}) => {
|
|
20173
|
+
if (textAnchor === "char-bottom") {
|
|
20174
|
+
// Align child's bottom with the char's bottom = the baseline.
|
|
20175
|
+
// The CSS spec says an inline-block with no text content has its baseline at its bottom margin edge.
|
|
20176
|
+
// So the browser's default placement already puts the child's bottom at the line's baseline.
|
|
20177
|
+
// No correction needed.
|
|
20178
|
+
return 0;
|
|
20179
|
+
}
|
|
20180
|
+
// The anchor's rendered rect corresponds to the surrounding text's line box:
|
|
20181
|
+
// top and bottom are the visual bounds of the line (including line-height).
|
|
20182
|
+
const anchorRect = anchorEl.getBoundingClientRect();
|
|
20183
|
+
|
|
20184
|
+
// Measure the child's current rect, then subtract any previously applied top correction
|
|
20185
|
+
// to recover its natural position — avoiding a style reset + reflow.
|
|
20186
|
+
const childRect = childEl.getBoundingClientRect();
|
|
20187
|
+
const childH = childRect.height;
|
|
20188
|
+
const previousTop = parseFloat(childEl.style.top) || 0;
|
|
20189
|
+
const childNaturalTop = childRect.top - previousTop;
|
|
20190
|
+
|
|
20191
|
+
// Compute desired child top Y based on textAnchor intention.
|
|
20192
|
+
let desiredChildTopY = 0;
|
|
20193
|
+
if (textAnchor === "line-top") {
|
|
20194
|
+
desiredChildTopY = anchorRect.top;
|
|
20195
|
+
} else if (textAnchor === "char-top") {
|
|
20196
|
+
const anchorStyle = getComputedStyle(anchorEl);
|
|
20197
|
+
const ctx = charTopCanvas.getContext("2d");
|
|
20198
|
+
ctx.font = `${anchorStyle.fontWeight} ${anchorStyle.fontSize} ${anchorStyle.fontFamily}`;
|
|
20199
|
+
const m = ctx.measureText("M");
|
|
20200
|
+
const baselineY = anchorRect.bottom - m.fontBoundingBoxDescent;
|
|
20201
|
+
desiredChildTopY = baselineY - m.actualBoundingBoxAscent;
|
|
20202
|
+
} else if (textAnchor === "center") {
|
|
20203
|
+
const anchorCenterY = (anchorRect.top + anchorRect.bottom) / 2;
|
|
20204
|
+
desiredChildTopY = anchorCenterY - childH / 2;
|
|
20205
|
+
} else if (textAnchor === "char-bottom") {
|
|
20206
|
+
// Already handled above (early return 0), but guard here for completeness.
|
|
20207
|
+
return 0;
|
|
20208
|
+
} else if (textAnchor === "line-bottom") {
|
|
20209
|
+
desiredChildTopY = anchorRect.bottom - childH;
|
|
20210
|
+
} else {
|
|
20211
|
+
return 0; // unknown textAnchor, no correction
|
|
20212
|
+
}
|
|
20213
|
+
return desiredChildTopY - childNaturalTop;
|
|
20214
|
+
};
|
|
20215
|
+
const charTopCanvas = document.createElement("canvas");
|
|
20216
|
+
|
|
19666
20217
|
const useInitialTextSelection = (ref, textSelection) => {
|
|
19667
20218
|
const deps = [];
|
|
19668
20219
|
if (Array.isArray(textSelection)) {
|
|
@@ -19763,7 +20314,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
|
|
|
19763
20314
|
};
|
|
19764
20315
|
|
|
19765
20316
|
installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
|
|
19766
|
-
const css$
|
|
20317
|
+
const css$_ = /* css */`
|
|
19767
20318
|
@layer navi {
|
|
19768
20319
|
.navi_text {
|
|
19769
20320
|
&[data-skeleton] {
|
|
@@ -19947,6 +20498,91 @@ const css$10 = /* css */`
|
|
|
19947
20498
|
opacity: 1;
|
|
19948
20499
|
}
|
|
19949
20500
|
}
|
|
20501
|
+
|
|
20502
|
+
/* ── Icon ── */
|
|
20503
|
+
|
|
20504
|
+
@layer navi {
|
|
20505
|
+
/* Ensure data attributes from box.jsx can win to update display */
|
|
20506
|
+
.navi_icon {
|
|
20507
|
+
display: inline-flex;
|
|
20508
|
+
box-sizing: border-box;
|
|
20509
|
+
max-width: 100%;
|
|
20510
|
+
/* An icon never grows past the box it sits in, so a glyph can never make
|
|
20511
|
+
a line of text taller than the text itself. lineOverflow="allow" opts
|
|
20512
|
+
out, for an icon that is an affordance rather than a character — a
|
|
20513
|
+
control's chevron or clear button, sized to be touched, not read. */
|
|
20514
|
+
max-height: 100%;
|
|
20515
|
+
|
|
20516
|
+
&[data-line-overflow="allow"] {
|
|
20517
|
+
max-height: none;
|
|
20518
|
+
}
|
|
20519
|
+
}
|
|
20520
|
+
}
|
|
20521
|
+
|
|
20522
|
+
.navi_icon {
|
|
20523
|
+
white-space: nowrap;
|
|
20524
|
+
vertical-align: inherit;
|
|
20525
|
+
|
|
20526
|
+
&[data-icon-char] {
|
|
20527
|
+
aspect-ratio: 1/1;
|
|
20528
|
+
min-width: 0;
|
|
20529
|
+
height: round(1em, 1px);
|
|
20530
|
+
max-height: round(1em, 1px);
|
|
20531
|
+
flex-grow: 0 !important;
|
|
20532
|
+
align-items: center;
|
|
20533
|
+
justify-content: center;
|
|
20534
|
+
|
|
20535
|
+
/* fillLine: measured on the line box (1lh) instead of the character box
|
|
20536
|
+
(1em). The icon still stays inside the line — it just uses all of it,
|
|
20537
|
+
which is what an icon standing on its own in a control's slot wants,
|
|
20538
|
+
where a glyph sitting among letters wants to match their size. */
|
|
20539
|
+
&[data-fill-line] {
|
|
20540
|
+
height: round(1lh, 1px);
|
|
20541
|
+
max-height: round(1lh, 1px);
|
|
20542
|
+
}
|
|
20543
|
+
|
|
20544
|
+
svg,
|
|
20545
|
+
img {
|
|
20546
|
+
width: 100%;
|
|
20547
|
+
height: 100%;
|
|
20548
|
+
}
|
|
20549
|
+
svg {
|
|
20550
|
+
overflow: visible;
|
|
20551
|
+
}
|
|
20552
|
+
}
|
|
20553
|
+
&[data-flow-inline] {
|
|
20554
|
+
width: 1em;
|
|
20555
|
+
height: 1em;
|
|
20556
|
+
}
|
|
20557
|
+
&[data-interactive] {
|
|
20558
|
+
cursor: pointer;
|
|
20559
|
+
}
|
|
20560
|
+
&[data-icon-text] {
|
|
20561
|
+
-webkit-font-smoothing: antialiased;
|
|
20562
|
+
text-rendering: optimizeLegibility;
|
|
20563
|
+
}
|
|
20564
|
+
}
|
|
20565
|
+
|
|
20566
|
+
.navi_icon > svg,
|
|
20567
|
+
.navi_icon > img {
|
|
20568
|
+
width: 100%;
|
|
20569
|
+
height: 100%;
|
|
20570
|
+
}
|
|
20571
|
+
.navi_icon[data-width-fixed] > svg,
|
|
20572
|
+
.navi_icon[data-width-fixed] > img {
|
|
20573
|
+
width: 100%;
|
|
20574
|
+
height: auto;
|
|
20575
|
+
}
|
|
20576
|
+
.navi_icon[data-height-fixed] > svg,
|
|
20577
|
+
.navi_icon[data-height-fixed] > img {
|
|
20578
|
+
width: auto;
|
|
20579
|
+
height: 100%;
|
|
20580
|
+
}
|
|
20581
|
+
.navi_icon[data-width-fixed][data-height-fixed] > svg,
|
|
20582
|
+
.navi_icon[data-width-fixed][data-height-fixed] > img {
|
|
20583
|
+
width: 100%;
|
|
20584
|
+
height: 100%;
|
|
20585
|
+
}
|
|
19950
20586
|
`;
|
|
19951
20587
|
const REGULAR_SPACE = jsx("span", {
|
|
19952
20588
|
"data-navi-space": "",
|
|
@@ -20129,6 +20765,7 @@ const shouldInjectSpacingBetween = (left, right) => {
|
|
|
20129
20765
|
* loading?: boolean,
|
|
20130
20766
|
* skeleton?: boolean,
|
|
20131
20767
|
* attachLastChild?: boolean,
|
|
20768
|
+
* emojiAsIcon?: boolean,
|
|
20132
20769
|
* preventSpaceUnderlines?: boolean,
|
|
20133
20770
|
* holdSpaceForStyle?: import("ignore:preact").JSX.CSSProperties,
|
|
20134
20771
|
* boldStable?: boolean,
|
|
@@ -20168,6 +20805,12 @@ const shouldInjectSpacingBetween = (left, right) => {
|
|
|
20168
20805
|
* truncation belongs outside the `Text` instead (see `docs/typography.md`).
|
|
20169
20806
|
* `Link` sets it on its own whenever it renders an end icon.
|
|
20170
20807
|
*
|
|
20808
|
+
* @param {boolean} [emojiAsIcon]
|
|
20809
|
+
* Renders every emoji found in the string children as an `Icon`, so it sits
|
|
20810
|
+
* in the line like a character and never makes the line taller than the
|
|
20811
|
+
* text. For free text a user typed (a message, a description) — the only
|
|
20812
|
+
* place an emoji is expected; see `docs/typography.md`.
|
|
20813
|
+
*
|
|
20171
20814
|
* @param {boolean} [preventSpaceUnderlines]
|
|
20172
20815
|
* Replaces real space characters between children with padding-based spaces.
|
|
20173
20816
|
* Useful inside `<a>` elements where browsers draw an underline under spaces.
|
|
@@ -20284,12 +20927,13 @@ const TextShrinkWrap = props => {
|
|
|
20284
20927
|
});
|
|
20285
20928
|
};
|
|
20286
20929
|
const TextUI = props => {
|
|
20287
|
-
import.meta.css = [css$
|
|
20930
|
+
import.meta.css = [css$_, "@jsenv/navi/src/text/text.jsx"];
|
|
20288
20931
|
let {
|
|
20289
20932
|
ref,
|
|
20290
20933
|
spacing,
|
|
20291
20934
|
preventSpaceUnderlines = false,
|
|
20292
20935
|
attachLastChild = false,
|
|
20936
|
+
emojiAsIcon = false,
|
|
20293
20937
|
boldStable,
|
|
20294
20938
|
holdSpaceForStyle,
|
|
20295
20939
|
capitalize,
|
|
@@ -20320,6 +20964,11 @@ const TextUI = props => {
|
|
|
20320
20964
|
} else {
|
|
20321
20965
|
children = applySpacingOnTextChildren(children, resolvedSpacing, defaultSpace);
|
|
20322
20966
|
}
|
|
20967
|
+
if (emojiAsIcon) {
|
|
20968
|
+
// After the spacing pass: an emoji glued to a word ("hello👋") must not
|
|
20969
|
+
// get a separator injected as if it were a child element.
|
|
20970
|
+
children = renderEmojiAsIcon(children);
|
|
20971
|
+
}
|
|
20323
20972
|
if (boldStable) {
|
|
20324
20973
|
const {
|
|
20325
20974
|
bold
|
|
@@ -20434,6 +21083,244 @@ const TextWithSelectRange = ({
|
|
|
20434
21083
|
});
|
|
20435
21084
|
};
|
|
20436
21085
|
|
|
21086
|
+
/**
|
|
21087
|
+
* Renders an icon — an inline SVG/emoji/text glyph that inherits the
|
|
21088
|
+
* surrounding text's `currentColor` and (by default) its font size, so it sits
|
|
21089
|
+
* on the text baseline like a character.
|
|
21090
|
+
*
|
|
21091
|
+
* Content comes from either `href` (references an external/sprite symbol via
|
|
21092
|
+
* `<use>`) or `children` (an inline `<svg>` element, or a string for a
|
|
21093
|
+
* text/emoji icon). All extra props are spread onto the underlying `Box`/`Text`
|
|
21094
|
+
* (sizing, spacing, color, className, data-attributes, …).
|
|
21095
|
+
*
|
|
21096
|
+
* Render mode is chosen automatically:
|
|
21097
|
+
* - `children` is a **string** → a text icon (`<Text data-icon-text>`).
|
|
21098
|
+
* - **sized** (an explicit `width`/`height`, or `flex`/`grid`) → a block icon
|
|
21099
|
+
* (`<Box square>`), laid out as its own box rather than inline.
|
|
21100
|
+
* - otherwise → an **inline char-like** icon that flows on the text baseline
|
|
21101
|
+
* (`data-icon-char`), aligned via `textAnchor`.
|
|
21102
|
+
*
|
|
21103
|
+
* Accessibility: an icon is treated as decorative (`aria-hidden`) by default
|
|
21104
|
+
* whenever it has no explicit size and no `onClick`; give it an explicit
|
|
21105
|
+
* `decorative={false}` (or make it interactive) when it conveys meaning.
|
|
21106
|
+
*
|
|
21107
|
+
* @param {object} props
|
|
21108
|
+
* @param {string} [props.href] - URL/id of an external SVG symbol, rendered via
|
|
21109
|
+
* `<svg><use href></svg>`. Mutually exclusive with meaningful `children`.
|
|
21110
|
+
* @param {import("ignore:preact").ComponentChildren} [props.children] - Inline icon
|
|
21111
|
+
* content: an `<svg>` element, or a string (renders as a text/emoji icon).
|
|
21112
|
+
* @param {boolean} [props.decorative] - Marks the icon `aria-hidden`. Defaults
|
|
21113
|
+
* to `true` for an unsized, non-interactive icon; pass `false` for a
|
|
21114
|
+
* meaning-bearing icon that needs to be exposed to assistive tech.
|
|
21115
|
+
* @param {(event: MouseEvent) => void} [props.onClick] - Makes the icon
|
|
21116
|
+
* interactive (`data-interactive`, pointer cursor) and non-decorative.
|
|
21117
|
+
* @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [props.textAnchor="center"]
|
|
21118
|
+
* - Vertical alignment within the surrounding text line for the inline
|
|
21119
|
+
* char-like mode, forwarded to `TextAnchor`: `"line-top"`/`"line-bottom"`
|
|
21120
|
+
* align to the line box edges, `"char-top"` to the ink ascent, `"center"`
|
|
21121
|
+
* centers on the line box, `"char-bottom"` sits on the baseline. See
|
|
21122
|
+
* `text_anchor.jsx`.
|
|
21123
|
+
* @param {{ size?: number, verticalAlign?: string }} [props.lineLayout] -
|
|
21124
|
+
* Describes the surrounding line context (font size / vertical-align),
|
|
21125
|
+
* forwarded to `TextAnchor` so it recomputes the vertical correction when
|
|
21126
|
+
* that context changes.
|
|
21127
|
+
* @param {string|number} [props.width] - Explicit width; `"auto"` clears it.
|
|
21128
|
+
* Any explicit size switches the icon to block (sized) mode.
|
|
21129
|
+
* @param {string|number} [props.height] - Explicit height; `"auto"` clears it.
|
|
21130
|
+
* @param {"allow"} [props.lineOverflow] - `"allow"` lets the icon be taller
|
|
21131
|
+
* than the box it sits in (a line of text, a control's slot) instead of being
|
|
21132
|
+
* capped by it. For an icon that is an affordance sized for the finger rather
|
|
21133
|
+
* than a character sized for reading.
|
|
21134
|
+
* @param {boolean} [props.fillLine] - Sizes the icon on the line box (1lh)
|
|
21135
|
+
* rather than on the character box (1em), so it uses the full height of the
|
|
21136
|
+
* line without leaving it. Unlike `lineOverflow`, the icon still never
|
|
21137
|
+
* exceeds the line.
|
|
21138
|
+
* @param {boolean} [props.square] - Keep a 1:1 box; combined with one explicit
|
|
21139
|
+
* dimension it fixes the other too.
|
|
21140
|
+
* @param {boolean} [props.circle] - Like `square`, plus a circular shape.
|
|
21141
|
+
* @param {string|number} [props.aspectRatio] - Fixes the second dimension from
|
|
21142
|
+
* the one explicit dimension.
|
|
21143
|
+
* @param {"x"|"y"|boolean} [props.flex] - Forces block/flex layout; auto-set to
|
|
21144
|
+
* `"x"` when the icon is sized.
|
|
21145
|
+
* @param {boolean} [props.grid] - Forces block/grid layout.
|
|
21146
|
+
* @param {string} [props.className] - Merged with the base `"navi_icon"` class.
|
|
21147
|
+
*/
|
|
21148
|
+
const Icon = ({
|
|
21149
|
+
href,
|
|
21150
|
+
children,
|
|
21151
|
+
decorative,
|
|
21152
|
+
onClick,
|
|
21153
|
+
textAnchor = "center",
|
|
21154
|
+
lineLayout,
|
|
21155
|
+
lineOverflow,
|
|
21156
|
+
fillLine,
|
|
21157
|
+
...props
|
|
21158
|
+
}) => {
|
|
21159
|
+
import.meta.css = [css$_, "@jsenv/navi/src/text/text.jsx"];
|
|
21160
|
+
const innerChildren = href ? jsx("svg", {
|
|
21161
|
+
width: "100%",
|
|
21162
|
+
height: "100%",
|
|
21163
|
+
children: jsx("use", {
|
|
21164
|
+
href: href
|
|
21165
|
+
})
|
|
21166
|
+
}) : children;
|
|
21167
|
+
let {
|
|
21168
|
+
flex,
|
|
21169
|
+
grid,
|
|
21170
|
+
width,
|
|
21171
|
+
height
|
|
21172
|
+
} = props;
|
|
21173
|
+
if (width === "auto") {
|
|
21174
|
+
width = undefined;
|
|
21175
|
+
}
|
|
21176
|
+
if (height === "auto") {
|
|
21177
|
+
height = undefined;
|
|
21178
|
+
}
|
|
21179
|
+
const hasExplicitWidth = width !== undefined;
|
|
21180
|
+
const hasExplicitHeight = height !== undefined;
|
|
21181
|
+
const widthFixed = hasExplicitWidth || hasExplicitHeight && (props.square || props.circle || props.aspectRatio);
|
|
21182
|
+
const heightFixed = hasExplicitHeight || hasExplicitWidth && (props.square || props.circle || props.aspectRatio);
|
|
21183
|
+
if (widthFixed || heightFixed) {
|
|
21184
|
+
if (flex === undefined) {
|
|
21185
|
+
flex = "x";
|
|
21186
|
+
}
|
|
21187
|
+
} else if (decorative === undefined && !onClick) {
|
|
21188
|
+
decorative = true;
|
|
21189
|
+
}
|
|
21190
|
+
const ariaProps = decorative ? {
|
|
21191
|
+
"aria-hidden": "true"
|
|
21192
|
+
} : {};
|
|
21193
|
+
const textRef = useRef();
|
|
21194
|
+
if (typeof children === "string") {
|
|
21195
|
+
return jsx(Text, {
|
|
21196
|
+
...props,
|
|
21197
|
+
...ariaProps,
|
|
21198
|
+
"data-icon-text": "",
|
|
21199
|
+
"data-line-overflow": lineOverflow,
|
|
21200
|
+
"data-fill-line": fillLine ? "" : undefined,
|
|
21201
|
+
children: children
|
|
21202
|
+
});
|
|
21203
|
+
}
|
|
21204
|
+
if (flex || grid) {
|
|
21205
|
+
return jsx(Box, {
|
|
21206
|
+
square: true,
|
|
21207
|
+
...props,
|
|
21208
|
+
...ariaProps,
|
|
21209
|
+
flex: flex,
|
|
21210
|
+
baseClassName: "navi_icon",
|
|
21211
|
+
"data-width-fixed": widthFixed ? "" : undefined,
|
|
21212
|
+
"data-height-fixed": heightFixed ? "" : undefined,
|
|
21213
|
+
"data-interactive": onClick ? "" : undefined,
|
|
21214
|
+
"data-line-overflow": lineOverflow,
|
|
21215
|
+
"data-fill-line": fillLine ? "" : undefined,
|
|
21216
|
+
onClick: onClick,
|
|
21217
|
+
children: innerChildren
|
|
21218
|
+
});
|
|
21219
|
+
}
|
|
21220
|
+
return jsx(TextAnchor, {
|
|
21221
|
+
childRef: textRef,
|
|
21222
|
+
textAnchor: textAnchor,
|
|
21223
|
+
textSize: props.size,
|
|
21224
|
+
lineLayout: lineLayout,
|
|
21225
|
+
children: jsxs(Text, {
|
|
21226
|
+
...props,
|
|
21227
|
+
...ariaProps,
|
|
21228
|
+
className: withPropsClassName("navi_icon", props.className),
|
|
21229
|
+
spacing: "pre",
|
|
21230
|
+
"data-icon-char": "",
|
|
21231
|
+
"data-line-overflow": lineOverflow,
|
|
21232
|
+
"data-fill-line": fillLine ? "" : undefined,
|
|
21233
|
+
"data-width-fixed": widthFixed ? "" : undefined,
|
|
21234
|
+
"data-height-fixed": heightFixed ? "" : undefined,
|
|
21235
|
+
"data-interactive": onClick ? "" : undefined,
|
|
21236
|
+
onClick: onClick,
|
|
21237
|
+
ref: textRef,
|
|
21238
|
+
children: [jsx("span", {
|
|
21239
|
+
style: "user-select:none",
|
|
21240
|
+
children: "\u200B"
|
|
21241
|
+
}), innerChildren]
|
|
21242
|
+
})
|
|
21243
|
+
});
|
|
21244
|
+
};
|
|
21245
|
+
|
|
21246
|
+
// An emoji-presentation character (🌸), or a pictogram forced into emoji
|
|
21247
|
+
// presentation by VS16 (❤️), followed by its skin-tone modifiers and ZWJ
|
|
21248
|
+
// sequence. A flag is two regional indicators that must stay together.
|
|
21249
|
+
const EMOJI_REGEX = /\p{Regional_Indicator}{2}|(?:\p{Emoji_Presentation}|[\p{Extended_Pictographic}--\p{Emoji_Presentation}]\uFE0F)(?:[\p{Emoji_Modifier}\uFE0F]|\u200D\p{Extended_Pictographic}\uFE0F?)*/gv;
|
|
21250
|
+
|
|
21251
|
+
/**
|
|
21252
|
+
* What `emojiAsIcon` does, for something that renders text without going
|
|
21253
|
+
* through `Text` (a callout's message): every emoji in the string children
|
|
21254
|
+
* comes back wrapped in an `Icon`. The system emoji fonts have a taller
|
|
21255
|
+
* ascent/descent than text fonts, so a raw emoji glyph makes its line taller
|
|
21256
|
+
* than the lines around it; inside an Icon it is capped at 1em and centered on
|
|
21257
|
+
* the line like any glyph icon.
|
|
21258
|
+
*
|
|
21259
|
+
* Children come back in the shape they arrived in: a string stays a string
|
|
21260
|
+
* when it holds no emoji, and the array is only built once a child actually
|
|
21261
|
+
* needs rewriting.
|
|
21262
|
+
*/
|
|
21263
|
+
const renderEmojiAsIcon = children => {
|
|
21264
|
+
if (typeof children === "string") {
|
|
21265
|
+
return renderEmojiInString(children);
|
|
21266
|
+
}
|
|
21267
|
+
const childArray = toChildArray(children);
|
|
21268
|
+
let result = null;
|
|
21269
|
+
let index = 0;
|
|
21270
|
+
for (const child of childArray) {
|
|
21271
|
+
if (typeof child === "string") {
|
|
21272
|
+
const rendered = renderEmojiInString(child);
|
|
21273
|
+
if (rendered !== child) {
|
|
21274
|
+
if (result === null) {
|
|
21275
|
+
result = childArray.slice(0, index);
|
|
21276
|
+
}
|
|
21277
|
+
for (const part of rendered) {
|
|
21278
|
+
result.push(part);
|
|
21279
|
+
}
|
|
21280
|
+
index++;
|
|
21281
|
+
continue;
|
|
21282
|
+
}
|
|
21283
|
+
}
|
|
21284
|
+
if (result !== null) {
|
|
21285
|
+
result.push(child);
|
|
21286
|
+
}
|
|
21287
|
+
index++;
|
|
21288
|
+
}
|
|
21289
|
+
if (result === null) {
|
|
21290
|
+
return children;
|
|
21291
|
+
}
|
|
21292
|
+
return result;
|
|
21293
|
+
};
|
|
21294
|
+
const renderEmojiInString = string => {
|
|
21295
|
+
let parts = null;
|
|
21296
|
+
let lastIndex = 0;
|
|
21297
|
+
for (const match of string.matchAll(EMOJI_REGEX)) {
|
|
21298
|
+
if (parts === null) {
|
|
21299
|
+
parts = [];
|
|
21300
|
+
}
|
|
21301
|
+
if (match.index > lastIndex) {
|
|
21302
|
+
parts.push(string.slice(lastIndex, match.index));
|
|
21303
|
+
}
|
|
21304
|
+
parts.push(jsx(Icon, {
|
|
21305
|
+
decorative: false,
|
|
21306
|
+
children: jsx("span", {
|
|
21307
|
+
children: match[0]
|
|
21308
|
+
})
|
|
21309
|
+
}));
|
|
21310
|
+
lastIndex = match.index + match[0].length;
|
|
21311
|
+
}
|
|
21312
|
+
if (parts === null) {
|
|
21313
|
+
return string;
|
|
21314
|
+
}
|
|
21315
|
+
if (lastIndex < string.length) {
|
|
21316
|
+
parts.push(string.slice(lastIndex));
|
|
21317
|
+
}
|
|
21318
|
+
return parts;
|
|
21319
|
+
};
|
|
21320
|
+
// A callout message is free text like any other; see callout.jsx for why it
|
|
21321
|
+
// cannot import this itself.
|
|
21322
|
+
setCalloutMessageTextRenderer(renderEmojiAsIcon);
|
|
21323
|
+
|
|
20437
21324
|
const LIGHT_ACCENT_ATTRIBUTE = "data-accent-light";
|
|
20438
21325
|
const VERY_LIGHT_ACCENT_ATTRIBUTE = "data-accent-very-light";
|
|
20439
21326
|
const DARK_CONTRAST_ATTRIBUTE = "data-accent-needs-dark-fg";
|
|
@@ -20577,105 +21464,6 @@ const useActionStatus = (action) => {
|
|
|
20577
21464
|
};
|
|
20578
21465
|
};
|
|
20579
21466
|
|
|
20580
|
-
/**
|
|
20581
|
-
* A variant of useLayoutEffect that accounts for ancestor <dialog>/<details>
|
|
20582
|
-
* or popover visibility.
|
|
20583
|
-
*
|
|
20584
|
-
* Motivation: some effects (auto-scroll, measurement, focus) only make sense
|
|
20585
|
-
* when the element is actually presented on screen. A plain useLayoutEffect
|
|
20586
|
-
* fires on mount even when the component is inside a closed <dialog>, a
|
|
20587
|
-
* collapsed <details>, or a hidden popover, where scroll and layout operations
|
|
20588
|
-
* are no-ops.
|
|
20589
|
-
*
|
|
20590
|
-
* Behavior:
|
|
20591
|
-
* - No <dialog>/<details>/[popover] ancestor → runs like a normal
|
|
20592
|
-
* useLayoutEffect with the provided deps.
|
|
20593
|
-
* - Inside a closed/hidden ancestor → skips the initial run; instead runs
|
|
20594
|
-
* the callback once the ancestor opens — see @jsenv/dom's own
|
|
20595
|
-
* observeAncestorOpenState for exactly how that's detected, and why it
|
|
20596
|
-
* matters that it happens before the browser paints.
|
|
20597
|
-
* - Inside an open ancestor → runs on mount AND every subsequent open.
|
|
20598
|
-
*
|
|
20599
|
-
* The callback's second argument is always a `navi_displayed` CustomEvent,
|
|
20600
|
-
* with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
|
|
20601
|
-
* - No <dialog>/<details>/[popover]/[aria-expanded] ancestor at all →
|
|
20602
|
-
* `{ ancestor: document, ancestorType: "document" }`.
|
|
20603
|
-
* - Otherwise → `{ ancestor: <the matched element>, ancestorType: "dialog"
|
|
20604
|
-
* | "popover" | "details" | "aria-expanded" }`.
|
|
20605
|
-
* `becauseAncestorOpened` distinguishes the two ways of coming on screen:
|
|
20606
|
-
* - true — the element was already mounted and the ancestor just opened,
|
|
20607
|
-
* revealing it along with everything else it holds. The opening has an
|
|
20608
|
-
* owner (the ancestor's own transferFocus/openEffect), and what it reveals
|
|
20609
|
-
* should defer to it — see use_auto_focus.js.
|
|
20610
|
-
* - false — the element was mounted just now, into a surface already on
|
|
20611
|
-
* screen (or into the plain document). Nothing else owns this appearance:
|
|
20612
|
-
* what the element says about itself (an autofocus, a measurement) is the
|
|
20613
|
-
* only word there is.
|
|
20614
|
-
*
|
|
20615
|
-
* Usage:
|
|
20616
|
-
* useDisplayedLayoutEffect(ref, () => {
|
|
20617
|
-
* scrollToSelected();
|
|
20618
|
-
* }, []);
|
|
20619
|
-
*/
|
|
20620
|
-
const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
20621
|
-
if (typeof callback !== "function") {
|
|
20622
|
-
throw new TypeError("useDisplayedLayoutEffect: callback is not a function");
|
|
20623
|
-
}
|
|
20624
|
-
|
|
20625
|
-
// Keep a stable ref so the open listener always calls the latest callback
|
|
20626
|
-
// without needing to be re-registered when deps change.
|
|
20627
|
-
const callbackRef = useRef(callback);
|
|
20628
|
-
callbackRef.current = callback;
|
|
20629
|
-
|
|
20630
|
-
// Run on mount (or when deps change) — but only if the element is visible.
|
|
20631
|
-
useLayoutEffect(() => {
|
|
20632
|
-
const el = ref.current;
|
|
20633
|
-
if (!el) {
|
|
20634
|
-
return;
|
|
20635
|
-
}
|
|
20636
|
-
const ancestor = closestOpenableAncestor(el);
|
|
20637
|
-
if (!ancestor) {
|
|
20638
|
-
callbackRef.current(el, createDisplayedEvent(document, false));
|
|
20639
|
-
return;
|
|
20640
|
-
}
|
|
20641
|
-
if (!isAncestorOpen(ancestor)) {
|
|
20642
|
-
// Ancestor is closed — skip now; the observeAncestorOpenState call
|
|
20643
|
-
// below will fire once it opens.
|
|
20644
|
-
return;
|
|
20645
|
-
}
|
|
20646
|
-
callbackRef.current(el, createDisplayedEvent(ancestor, false));
|
|
20647
|
-
}, deps);
|
|
20648
|
-
|
|
20649
|
-
// Re-run every time the ancestor opens.
|
|
20650
|
-
useLayoutEffect(() => {
|
|
20651
|
-
const el = ref.current;
|
|
20652
|
-
if (!el) {
|
|
20653
|
-
return undefined;
|
|
20654
|
-
}
|
|
20655
|
-
const ancestor = closestOpenableAncestor(el);
|
|
20656
|
-
if (!ancestor) {
|
|
20657
|
-
return undefined;
|
|
20658
|
-
}
|
|
20659
|
-
return observeAncestorOpenState(ancestor, ({ isOpen }) => {
|
|
20660
|
-
if (!isOpen) {
|
|
20661
|
-
return;
|
|
20662
|
-
}
|
|
20663
|
-
const lastEl = ref.current;
|
|
20664
|
-
callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
|
|
20665
|
-
});
|
|
20666
|
-
}, []);
|
|
20667
|
-
};
|
|
20668
|
-
|
|
20669
|
-
const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
|
|
20670
|
-
return new CustomEvent("navi_displayed", {
|
|
20671
|
-
detail: {
|
|
20672
|
-
ancestor,
|
|
20673
|
-
ancestorType: getAncestorOpenType(ancestor),
|
|
20674
|
-
becauseAncestorOpened,
|
|
20675
|
-
},
|
|
20676
|
-
});
|
|
20677
|
-
};
|
|
20678
|
-
|
|
20679
21467
|
/**
|
|
20680
21468
|
* Decides which element receives focus when a container (popover, dialog, …)
|
|
20681
21469
|
* opens, and gives it back to where it came from when the container closes.
|
|
@@ -23467,116 +24255,6 @@ registerNaviCommand("--navi-unselect", (source, event) => {
|
|
|
23467
24255
|
};
|
|
23468
24256
|
});
|
|
23469
24257
|
|
|
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
24258
|
/**
|
|
23581
24259
|
* How a control tells the labels pointing at it what it is (disabled, readOnly,
|
|
23582
24260
|
* required) and when it goes away.
|
|
@@ -25798,18 +26476,18 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
25798
26476
|
if (child !== firstChildControllerRef.current) {
|
|
25799
26477
|
return;
|
|
25800
26478
|
}
|
|
25801
|
-
if (
|
|
25802
|
-
silent &&
|
|
25803
|
-
uiStateHoldsNothing(child.uiState) &&
|
|
25804
|
-
!uiStateHoldsNothing(s.realUIStateController.uiState)
|
|
25805
|
-
) {
|
|
26479
|
+
if (silent && uiStateHoldsNothing(child.uiState)) {
|
|
25806
26480
|
// A silent sync means the child's own structure changed (children
|
|
25807
26481
|
// mounted/unmounted), not that the user acted. A child that ends up
|
|
25808
26482
|
// with no value there is one that currently *cannot* express one —
|
|
25809
26483
|
// a <List loading> holds no items yet, a popup whose items are gone
|
|
25810
26484
|
// aggregates to the empty array its stateType falls back to — which
|
|
25811
26485
|
// must not read as the user clearing the picker, nor fire its
|
|
25812
|
-
// uiAction.
|
|
26486
|
+
// uiAction. And when the picker holds nothing either, there is
|
|
26487
|
+
// still nothing to adopt: the sync would only respell one nothing
|
|
26488
|
+
// as another (an array picker's [] becoming undefined, say) and
|
|
26489
|
+
// hand that to uiAction — an empty array picker opened its popup
|
|
26490
|
+
// and told its owner the value changed.
|
|
25813
26491
|
return;
|
|
25814
26492
|
}
|
|
25815
26493
|
updatingRef.current = true;
|
|
@@ -25822,7 +26500,18 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
25822
26500
|
detail: {},
|
|
25823
26501
|
});
|
|
25824
26502
|
chainEvent(propagateUpEvent, e);
|
|
25825
|
-
s
|
|
26503
|
+
// What the popup aggregates arrives in the popup's terms, where an
|
|
26504
|
+
// empty multiple list is `undefined`. The picker answers a question of
|
|
26505
|
+
// its own shape (see resolveEmptyUIState): a <Picker type="array">
|
|
26506
|
+
// whose last item was unselected holds [], the same thing clearing it
|
|
26507
|
+
// leaves — not a value that changes type on its owner the moment it
|
|
26508
|
+
// empties.
|
|
26509
|
+
const { emptyUIState } = s.realUIStateController;
|
|
26510
|
+
const uiStateToAdopt =
|
|
26511
|
+
child.uiState === undefined && emptyUIState !== undefined
|
|
26512
|
+
? emptyUIState
|
|
26513
|
+
: child.uiState;
|
|
26514
|
+
s.realUIStateController.setUIState(uiStateToAdopt, propagateUpEvent);
|
|
25826
26515
|
updatingRef.current = false;
|
|
25827
26516
|
},
|
|
25828
26517
|
};
|
|
@@ -26732,11 +27421,29 @@ const useControlProps = (props, {
|
|
|
26732
27421
|
}
|
|
26733
27422
|
return dispatched;
|
|
26734
27423
|
};
|
|
26735
|
-
|
|
27424
|
+
|
|
27425
|
+
// What the caller wrote runs from inside the gate when this control is an
|
|
27426
|
+
// own target: a plain `onClick` is DOM, and it would fire from a cross drawn
|
|
27427
|
+
// greyed by the read-only control the affordance sits in (see own_target.js).
|
|
27428
|
+
const callerHandlerIsGated = Boolean(props.ownTarget);
|
|
27429
|
+
const gateCallerHandler = (handler, e) => {
|
|
27430
|
+
if (!handler) {
|
|
27431
|
+
return undefined;
|
|
27432
|
+
}
|
|
27433
|
+
if (callerHandlerIsGated) {
|
|
27434
|
+
return handler;
|
|
27435
|
+
}
|
|
27436
|
+
handler(e);
|
|
27437
|
+
return undefined;
|
|
27438
|
+
};
|
|
27439
|
+
const applyEventReaction = (eventName, e, callerHandler) => {
|
|
26736
27440
|
const defaultEventReactionDefinition = defaultEventReactionDefinitions?.[eventName];
|
|
26737
27441
|
const customEventReactionDefinition = eventReactionDefinitions?.[eventName];
|
|
26738
27442
|
const reaction = customEventReactionDefinition?.(e) ?? defaultEventReactionDefinition?.(e);
|
|
26739
27443
|
if (!reaction) {
|
|
27444
|
+
// No reaction means no gate: there is nothing here that could refuse the
|
|
27445
|
+
// caller's handler, so withholding it would only lose it.
|
|
27446
|
+
callerHandler?.(e);
|
|
26740
27447
|
return false;
|
|
26741
27448
|
}
|
|
26742
27449
|
const {
|
|
@@ -26761,19 +27468,18 @@ const useControlProps = (props, {
|
|
|
26761
27468
|
prevented?.();
|
|
26762
27469
|
},
|
|
26763
27470
|
allowed: () => {
|
|
27471
|
+
callerHandler?.(e);
|
|
26764
27472
|
allowed?.();
|
|
26765
27473
|
},
|
|
26766
27474
|
always
|
|
26767
27475
|
});
|
|
26768
27476
|
};
|
|
26769
27477
|
const onMouseDown = e => {
|
|
26770
|
-
props.onMouseDown
|
|
26771
|
-
applyEventReaction("mouseDown", e);
|
|
27478
|
+
applyEventReaction("mouseDown", e, gateCallerHandler(props.onMouseDown, e));
|
|
26772
27479
|
transferFocusToTarget(e);
|
|
26773
27480
|
};
|
|
26774
27481
|
const onClick = e => {
|
|
26775
|
-
props.onClick
|
|
26776
|
-
applyEventReaction("click", e);
|
|
27482
|
+
applyEventReaction("click", e, gateCallerHandler(props.onClick, e));
|
|
26777
27483
|
transferFocusToTarget(e);
|
|
26778
27484
|
};
|
|
26779
27485
|
const onKeyDown = e => {
|
|
@@ -27296,6 +28002,13 @@ const useInteractiveProps = (props, {
|
|
|
27296
28002
|
} = props;
|
|
27297
28003
|
const [controlRootProps, controlHostProps] = splitControlProps(props);
|
|
27298
28004
|
controlRootProps["navi-control"] = controlInfo.controlType;
|
|
28005
|
+
if (props.ownTarget) {
|
|
28006
|
+
// One attribute, in the DOM, because that is where the claim is read from —
|
|
28007
|
+
// by the controls above and by the gesture readers below (see
|
|
28008
|
+
// own_target.js). The prop is the ergonomic form of it and nothing more: an
|
|
28009
|
+
// element an application draws itself writes the same attribute by hand.
|
|
28010
|
+
controlRootProps[OWN_TARGET_ATTRIBUTE] = typeof props.ownTarget === "string" ? props.ownTarget : "";
|
|
28011
|
+
}
|
|
27299
28012
|
const {
|
|
27300
28013
|
"navi-control-proxy-for": naviProxyFor
|
|
27301
28014
|
} = props;
|
|
@@ -27330,10 +28043,10 @@ const useInteractiveProps = (props, {
|
|
|
27330
28043
|
});
|
|
27331
28044
|
}
|
|
27332
28045
|
{
|
|
27333
|
-
const
|
|
27334
|
-
const
|
|
28046
|
+
const controlDisabledFromAbove = useContext(DisabledContext);
|
|
28047
|
+
const controlReadOnlyFromAbove = useContext(ReadOnlyContext);
|
|
27335
28048
|
const controlRequired = useContext(RequiredContext);
|
|
27336
|
-
const
|
|
28049
|
+
const controlLoadingFromAbove = useContext(LoadingContext$1);
|
|
27337
28050
|
const parentActionRequester = useContext(ActionRequesterContext);
|
|
27338
28051
|
const actionStatus = useActionStatus(boundAction);
|
|
27339
28052
|
const {
|
|
@@ -27343,6 +28056,16 @@ const useInteractiveProps = (props, {
|
|
|
27343
28056
|
loading,
|
|
27344
28057
|
optimistic
|
|
27345
28058
|
} = props;
|
|
28059
|
+
|
|
28060
|
+
// `ownTarget="always"`: an affordance that writes nothing to the control it
|
|
28061
|
+
// sits in has no business inheriting that control's state — a diskette
|
|
28062
|
+
// saving a row into the reader's own address book stays pressable on a game
|
|
28063
|
+
// nobody may edit. Its own props still hold; only what came from above is
|
|
28064
|
+
// dropped (see own_target.js for the three modes).
|
|
28065
|
+
const zoneStateApplies = !ownTargetIgnoresZoneState(props.ownTarget);
|
|
28066
|
+
const controlDisabled = zoneStateApplies && controlDisabledFromAbove;
|
|
28067
|
+
const controlReadOnly = zoneStateApplies && controlReadOnlyFromAbove;
|
|
28068
|
+
const controlLoading = zoneStateApplies && controlLoadingFromAbove;
|
|
27346
28069
|
const disabledResolved = disabled || controlDisabled;
|
|
27347
28070
|
const requiredResolved = required || controlRequired;
|
|
27348
28071
|
// Busy because the group above is running the action THIS control asked
|
|
@@ -27354,7 +28077,7 @@ const useInteractiveProps = (props, {
|
|
|
27354
28077
|
// (`maxLengthGuard`) and this one would make it longer: it can be pointed
|
|
27355
28078
|
// at, focused and pressed — and answers why (see readonly_constraint.js) —
|
|
27356
28079
|
// but cannot be taken.
|
|
27357
|
-
const readOnlyFromParentMaxLengthGuard = Boolean(uiStateController.parentUIStateController?.isChildBlockedByMaxLengthGuard?.(uiStateController));
|
|
28080
|
+
const readOnlyFromParentMaxLengthGuard = Boolean(zoneStateApplies && uiStateController.parentUIStateController?.isChildBlockedByMaxLengthGuard?.(uiStateController));
|
|
27358
28081
|
const readOnlyBase = readOnly || controlReadOnly || loadingBase || readOnlyFromParentMaxLengthGuard || controlInfo.readOnlyUncontrolled;
|
|
27359
28082
|
// An optimistic control trusts its action to succeed: the state the user
|
|
27360
28083
|
// just set stays visible and interactive while the action runs — no
|
|
@@ -27788,7 +28511,7 @@ const getAssociatedLabels = element => {
|
|
|
27788
28511
|
return Array.from(element.labels);
|
|
27789
28512
|
};
|
|
27790
28513
|
|
|
27791
|
-
installImportMetaCssBuild(import.meta);const css
|
|
28514
|
+
installImportMetaCssBuild(import.meta);const css$Z = /* css */`
|
|
27792
28515
|
@layer navi {
|
|
27793
28516
|
.navi_button {
|
|
27794
28517
|
--button-border-radius: var(--navi-control-border-radius);
|
|
@@ -28219,7 +28942,7 @@ installImportMetaCssBuild(import.meta);const css$$ = /* css */`
|
|
|
28219
28942
|
}
|
|
28220
28943
|
`;
|
|
28221
28944
|
const ButtonUI = props => {
|
|
28222
|
-
import.meta.css = [css
|
|
28945
|
+
import.meta.css = [css$Z, "@jsenv/navi/src/control/input/button_ui.jsx"];
|
|
28223
28946
|
const {
|
|
28224
28947
|
ref,
|
|
28225
28948
|
// href/link
|
|
@@ -28232,6 +28955,7 @@ const ButtonUI = props => {
|
|
|
28232
28955
|
icon,
|
|
28233
28956
|
cta,
|
|
28234
28957
|
spacing,
|
|
28958
|
+
emojiAsIcon = true,
|
|
28235
28959
|
// Whether the button draws the loading outline itself. A button that is
|
|
28236
28960
|
// one half of a bigger control says no: what is busy is the control, and
|
|
28237
28961
|
// the outline belongs around the whole of it (see split_button.jsx).
|
|
@@ -28289,6 +29013,7 @@ const ButtonUI = props => {
|
|
|
28289
29013
|
|
|
28290
29014
|
type: "button",
|
|
28291
29015
|
spacing: undefined,
|
|
29016
|
+
emojiAsIcon: undefined,
|
|
28292
29017
|
cta: undefined,
|
|
28293
29018
|
pressEffect: undefined,
|
|
28294
29019
|
loadingOutline: undefined,
|
|
@@ -28342,6 +29067,7 @@ const ButtonUI = props => {
|
|
|
28342
29067
|
...controlChildrenWrapperProps,
|
|
28343
29068
|
children: jsx(ButtonContent, {
|
|
28344
29069
|
spacing: spacing,
|
|
29070
|
+
emojiAsIcon: emojiAsIcon,
|
|
28345
29071
|
children: children
|
|
28346
29072
|
})
|
|
28347
29073
|
})]
|
|
@@ -28349,6 +29075,7 @@ const ButtonUI = props => {
|
|
|
28349
29075
|
};
|
|
28350
29076
|
const ButtonContent = ({
|
|
28351
29077
|
spacing,
|
|
29078
|
+
emojiAsIcon,
|
|
28352
29079
|
children
|
|
28353
29080
|
}) => {
|
|
28354
29081
|
const boxForwardedProps = useContext(BoxForwardedPropsContext);
|
|
@@ -28356,6 +29083,7 @@ const ButtonContent = ({
|
|
|
28356
29083
|
...boxForwardedProps,
|
|
28357
29084
|
display: "inherit",
|
|
28358
29085
|
spacing: spacing,
|
|
29086
|
+
emojiAsIcon: emojiAsIcon,
|
|
28359
29087
|
className: "navi_button_content",
|
|
28360
29088
|
children: [children, jsx(ButtonShadow, {})]
|
|
28361
29089
|
});
|
|
@@ -28412,6 +29140,7 @@ const ButtonFirstResolver = props => {
|
|
|
28412
29140
|
const Next = useNextResolver();
|
|
28413
29141
|
const defaultRef = useRef(null);
|
|
28414
29142
|
props.ref = props.ref || defaultRef;
|
|
29143
|
+
const ownTargetHidden = useOwnTargetHidden(props);
|
|
28415
29144
|
|
|
28416
29145
|
// Attached to the element rather than kept as a prop: the action a button
|
|
28417
29146
|
// requests is not always run by the button (a submit button hands the send to
|
|
@@ -28421,6 +29150,9 @@ const ButtonFirstResolver = props => {
|
|
|
28421
29150
|
message: props.confirm,
|
|
28422
29151
|
content: props.confirmPopupContent
|
|
28423
29152
|
});
|
|
29153
|
+
if (ownTargetHidden) {
|
|
29154
|
+
return null;
|
|
29155
|
+
}
|
|
28424
29156
|
return jsx(Next, {
|
|
28425
29157
|
...props
|
|
28426
29158
|
});
|
|
@@ -28511,6 +29243,26 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
28511
29243
|
children: naviI18n("button.open")
|
|
28512
29244
|
})
|
|
28513
29245
|
};
|
|
29246
|
+
|
|
29247
|
+
/**
|
|
29248
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
29249
|
+
* ownTarget?: boolean | "refuse" | "always",
|
|
29250
|
+
* emojiAsIcon?: boolean,
|
|
29251
|
+
* [key: string]: any,
|
|
29252
|
+
* }>}
|
|
29253
|
+
* @param {boolean} [emojiAsIcon=true] Renders the emoji of the label as icons
|
|
29254
|
+
* so the button keeps the height of its text — `Text`'s prop, on by default
|
|
29255
|
+
* here. Pass `false` to let an emoji draw at its natural size.
|
|
29256
|
+
* @param {boolean|"refuse"|"always"} [ownTarget] A real target inside a zone
|
|
29257
|
+
* that belongs to another control — a chip's cross on a picker's façade, an
|
|
29258
|
+
* eye on a pressable row, a diskette inside a slide that travels. The press is
|
|
29259
|
+
* this button's alone (no travel starts, no popup opens, no navi control above
|
|
29260
|
+
* answers) and its `onClick` waits for its own interaction gate instead of
|
|
29261
|
+
* firing from the DOM. What it does where the zone is read-only, disabled or
|
|
29262
|
+
* busy depends on whether it WRITES to the control it sits in: it goes by
|
|
29263
|
+
* default, `"refuse"` keeps it and refuses with a callout, `"always"` ignores
|
|
29264
|
+
* the zone's state entirely — for a gesture that never touched that control.
|
|
29265
|
+
*/
|
|
28514
29266
|
const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
|
|
28515
29267
|
|
|
28516
29268
|
// How long a popup waits before handing the focus to a field, when giving it
|
|
@@ -29930,7 +30682,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
29930
30682
|
* reaches the real container.
|
|
29931
30683
|
*/
|
|
29932
30684
|
let openLocalDialogCount = 0;
|
|
29933
|
-
const css$
|
|
30685
|
+
const css$Y = /* css */`
|
|
29934
30686
|
@layer navi {
|
|
29935
30687
|
.navi_dialog {
|
|
29936
30688
|
/* Min gap between the dialog and the edges of its container. Written
|
|
@@ -30071,6 +30823,10 @@ const css$_ = /* css */`
|
|
|
30071
30823
|
padding: 0;
|
|
30072
30824
|
flex-direction: column;
|
|
30073
30825
|
|
|
30826
|
+
/* A new surface writes in its own ink, not in its container's — see
|
|
30827
|
+
--navi-popup-color. Declared here rather than left to the UA's own
|
|
30828
|
+
dialog { color: CanvasText } so the ink is themed along with the paper. */
|
|
30829
|
+
color: var(--navi-popup-color);
|
|
30074
30830
|
background-color: var(--dialog-background-color);
|
|
30075
30831
|
border-width: var(--dialog-border-width);
|
|
30076
30832
|
border-style: solid;
|
|
@@ -30514,7 +31270,7 @@ const css$_ = /* css */`
|
|
|
30514
31270
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
30515
31271
|
*/
|
|
30516
31272
|
const Dialog = props => {
|
|
30517
|
-
import.meta.css = [css$
|
|
31273
|
+
import.meta.css = [css$Y, "@jsenv/navi/src/layout/dialog.jsx"];
|
|
30518
31274
|
if (props.openController) {
|
|
30519
31275
|
return jsx(ControlledDialog, {
|
|
30520
31276
|
...props
|
|
@@ -31506,7 +32262,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
31506
32262
|
* and applied.
|
|
31507
32263
|
*/
|
|
31508
32264
|
let openLocalPopoverCount = 0;
|
|
31509
|
-
const css$
|
|
32265
|
+
const css$X = /* css */`
|
|
31510
32266
|
@layer navi {
|
|
31511
32267
|
.navi_popover {
|
|
31512
32268
|
/* soft: user-configurable preferred max-height. Kept as a *default*
|
|
@@ -31600,6 +32356,10 @@ const css$Z = /* css */`
|
|
|
31600
32356
|
var(--x-popover-max-height)
|
|
31601
32357
|
);
|
|
31602
32358
|
max-height: var(--x-popover-max-height);
|
|
32359
|
+
/* A new surface writes in its own ink, not in its container's — see
|
|
32360
|
+
--navi-popup-color. The UA only resets color on a [popover] element,
|
|
32361
|
+
and the local renderer is a plain div. */
|
|
32362
|
+
color: var(--navi-popup-color);
|
|
31603
32363
|
background-color: var(--popover-background-color);
|
|
31604
32364
|
border-width: var(--popover-border-width);
|
|
31605
32365
|
border-style: solid;
|
|
@@ -31962,7 +32722,7 @@ const css$Z = /* css */`
|
|
|
31962
32722
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
31963
32723
|
*/
|
|
31964
32724
|
const Popover = props => {
|
|
31965
|
-
import.meta.css = [css$
|
|
32725
|
+
import.meta.css = [css$X, "@jsenv/navi/src/layout/popover.jsx"];
|
|
31966
32726
|
if (props.openController) {
|
|
31967
32727
|
return jsx(ControlledPopover, {
|
|
31968
32728
|
...props
|
|
@@ -32990,7 +33750,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
32990
33750
|
* event, and a caller replacing the body entirely then has one protocol to
|
|
32991
33751
|
* follow — `--navi-confirm` for yes, anything that closes for no.
|
|
32992
33752
|
*/
|
|
32993
|
-
const css$
|
|
33753
|
+
const css$W = /* css */`
|
|
32994
33754
|
/* The width lives on the body rather than on the popup, so that custom
|
|
32995
33755
|
content (which replaces this body entirely) sizes itself instead of
|
|
32996
33756
|
inheriting a ceiling meant for a sentence-long question. */
|
|
@@ -33127,7 +33887,7 @@ const ConfirmPopup = ({
|
|
|
33127
33887
|
onAnswer,
|
|
33128
33888
|
onClosed
|
|
33129
33889
|
}) => {
|
|
33130
|
-
import.meta.css = [css$
|
|
33890
|
+
import.meta.css = [css$W, "@jsenv/navi/src/action/confirm_popup.jsx"];
|
|
33131
33891
|
const {
|
|
33132
33892
|
mode,
|
|
33133
33893
|
confirmLabel,
|
|
@@ -33211,7 +33971,7 @@ const defaultBody = (message, {
|
|
|
33211
33971
|
});
|
|
33212
33972
|
};
|
|
33213
33973
|
|
|
33214
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
33974
|
+
installImportMetaCssBuild(import.meta);const css$V = /* css */`
|
|
33215
33975
|
.action_error {
|
|
33216
33976
|
margin-top: 0;
|
|
33217
33977
|
margin-bottom: 20px;
|
|
@@ -33236,7 +33996,7 @@ const ActionRenderer = ({
|
|
|
33236
33996
|
children,
|
|
33237
33997
|
disabled
|
|
33238
33998
|
}) => {
|
|
33239
|
-
import.meta.css = [css$
|
|
33999
|
+
import.meta.css = [css$V, "@jsenv/navi/src/action/action_renderer.jsx"];
|
|
33240
34000
|
if (action === undefined) {
|
|
33241
34001
|
throw new Error("ActionRenderer requires an action to render, but none was provided.");
|
|
33242
34002
|
}
|
|
@@ -38861,7 +39621,7 @@ const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
|
|
|
38861
39621
|
// the root pictures must NOT move (they carry the whole viewport, blank bands
|
|
38862
39622
|
// included).
|
|
38863
39623
|
|
|
38864
|
-
const css$
|
|
39624
|
+
const css$U = /* css */`
|
|
38865
39625
|
/* The marked region is a picture of its own for the length of a transition of
|
|
38866
39626
|
OURS, and only then — the name is what makes the pages a picture the
|
|
38867
39627
|
movement below can carry.
|
|
@@ -39290,7 +40050,7 @@ const RouteTransitionArea = ({
|
|
|
39290
40050
|
children,
|
|
39291
40051
|
...rest
|
|
39292
40052
|
}) => {
|
|
39293
|
-
import.meta.css = [css$
|
|
40053
|
+
import.meta.css = [css$U, "@jsenv/navi/src/nav/route_transition.jsx"];
|
|
39294
40054
|
const props = {
|
|
39295
40055
|
...rest,
|
|
39296
40056
|
[TRANSITION_AREA_ATTRIBUTE]: ""
|
|
@@ -39347,7 +40107,7 @@ const RouteTransitionArea = ({
|
|
|
39347
40107
|
* @returns {() => void} remove this relation.
|
|
39348
40108
|
*/
|
|
39349
40109
|
const defineRouteTransition = (from, to, transition) => {
|
|
39350
|
-
import.meta.css = [css$
|
|
40110
|
+
import.meta.css = [css$U, "@jsenv/navi/src/nav/route_transition.jsx"];
|
|
39351
40111
|
const {
|
|
39352
40112
|
type,
|
|
39353
40113
|
duration
|
|
@@ -39383,7 +40143,7 @@ const defineRouteTransition = (from, to, transition) => {
|
|
|
39383
40143
|
* @returns {() => void} remove this default.
|
|
39384
40144
|
*/
|
|
39385
40145
|
const defineRouteDefaultTransition = transition => {
|
|
39386
|
-
import.meta.css = [css$
|
|
40146
|
+
import.meta.css = [css$U, "@jsenv/navi/src/nav/route_transition.jsx"];
|
|
39387
40147
|
const value = normalizeTransition(transition);
|
|
39388
40148
|
defaultTransition = value;
|
|
39389
40149
|
return () => {
|
|
@@ -39984,7 +40744,7 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
|
|
|
39984
40744
|
const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
|
|
39985
40745
|
// The name the box wears while it travels, and only then (see nameForTravel).
|
|
39986
40746
|
const TRAVEL_NAME = "navi-route-travel";
|
|
39987
|
-
const css$
|
|
40747
|
+
const css$T = /* css */`
|
|
39988
40748
|
/* The name that makes the page inside this box a picture of its own during a
|
|
39989
40749
|
transition — rather than part of the one big picture the document takes, so
|
|
39990
40750
|
the two pages can move past each other while everything else stays where it
|
|
@@ -40374,7 +41134,7 @@ const RouteTravel = ({
|
|
|
40374
41134
|
children,
|
|
40375
41135
|
...rest
|
|
40376
41136
|
}) => {
|
|
40377
|
-
import.meta.css = [css$
|
|
41137
|
+
import.meta.css = [css$T, "@jsenv/navi/src/nav/route_travel.jsx"];
|
|
40378
41138
|
const elementRef = useRef();
|
|
40379
41139
|
const gestureRef = useRef(null);
|
|
40380
41140
|
// The travel in hand: the transition keeping the picture of the page being
|
|
@@ -42935,402 +43695,6 @@ const PhoneSvg = () => {
|
|
|
42935
43695
|
});
|
|
42936
43696
|
};
|
|
42937
43697
|
|
|
42938
|
-
installImportMetaCssBuild(import.meta);// # TextAnchor — how it works
|
|
42939
|
-
const css$U = /* css */`
|
|
42940
|
-
.navi_text_anchor {
|
|
42941
|
-
vertical-align: baseline;
|
|
42942
|
-
user-select: none;
|
|
42943
|
-
overflow: hidden;
|
|
42944
|
-
}
|
|
42945
|
-
`;
|
|
42946
|
-
|
|
42947
|
-
/**
|
|
42948
|
-
* Positions children vertically relative to the surrounding text, correcting for font-size differences.
|
|
42949
|
-
*
|
|
42950
|
-
* Place this component around any inline element whose font-size differs from the surrounding text.
|
|
42951
|
-
* It renders an invisible anchor that inherits the surrounding text's font metrics, then shifts
|
|
42952
|
-
* the child so that its visual position matches the requested `textAnchor` value — regardless of
|
|
42953
|
-
* font-size, display type (inline, inline-block, inline-flex…), or the active `vertical-align`.
|
|
42954
|
-
*
|
|
42955
|
-
* @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [textAnchor="char-bottom"]
|
|
42956
|
-
* - `"line-top"` — child top aligns with the top of the surrounding line box
|
|
42957
|
-
* - `"char-top"` — child top aligns with the top of visible characters (ink ascent)
|
|
42958
|
-
* - `"center"` — child is vertically centered on the surrounding line box
|
|
42959
|
-
* - `"char-bottom"` — child bottom aligns to the text baseline (no correction, browser default)
|
|
42960
|
-
* - `"line-bottom"` — child bottom aligns with the bottom of the surrounding line box
|
|
42961
|
-
* @param {{ size?: number, verticalAlign?: string }} [lineLayout]
|
|
42962
|
-
* Describes the surrounding line context. Used as layout-effect dependencies so the correction
|
|
42963
|
-
* reruns when the surrounding text's font-size or vertical-align changes.
|
|
42964
|
-
* @param {import("ignore:preact").RefObject} childRef — ref on the child element to reposition
|
|
42965
|
-
*/
|
|
42966
|
-
const TextAnchor = ({
|
|
42967
|
-
childRef,
|
|
42968
|
-
children,
|
|
42969
|
-
textAnchor = "char-bottom",
|
|
42970
|
-
textKey,
|
|
42971
|
-
textSize,
|
|
42972
|
-
lineLayout
|
|
42973
|
-
}) => {
|
|
42974
|
-
import.meta.css = [css$U, "@jsenv/navi/src/text/text_anchor.jsx"];
|
|
42975
|
-
const anchorRef = useRef();
|
|
42976
|
-
|
|
42977
|
-
// Plain useLayoutEffect would also fire while an ancestor dialog/popover
|
|
42978
|
-
// (e.g. a closed SidePanel) is still display:none — every rect involved
|
|
42979
|
-
// reads 0×0 at that point, so the math trivially (and wrongly) resolves
|
|
42980
|
-
// to topOffset 0: not a real "no correction needed" result, just a
|
|
42981
|
-
// zero-by-zero coincidence that happens to look fine only because it
|
|
42982
|
-
// leaves the browser's own default alignment untouched. The real
|
|
42983
|
-
// correction then only gets applied later, on whatever unrelated
|
|
42984
|
-
// re-render next happens to change one of this effect's own deps —
|
|
42985
|
-
// which reads as the child "jumping" even though nothing about its own
|
|
42986
|
-
// geometry changed. useDisplayedLayoutEffect skips the initial run in
|
|
42987
|
-
// that case and reruns once the ancestor actually opens instead.
|
|
42988
|
-
useDisplayedLayoutEffect(anchorRef, anchorEl => {
|
|
42989
|
-
const childEl = childRef.current;
|
|
42990
|
-
if (!anchorEl || !childEl) {
|
|
42991
|
-
return;
|
|
42992
|
-
}
|
|
42993
|
-
// Only correct when the anchor lives in an inline formatting context.
|
|
42994
|
-
// If the parent is a flex/grid container, inline layout rules don't apply
|
|
42995
|
-
// and our font-metrics model is invalid.
|
|
42996
|
-
const parentDisplay = getComputedStyle(anchorEl.parentElement).display;
|
|
42997
|
-
if (parentDisplay !== "inline" && parentDisplay !== "inline-block" && parentDisplay !== "block") {
|
|
42998
|
-
// we must hide the anchor otherwise it would affect layout without providing any benefit (would trigger flex gap for instance)
|
|
42999
|
-
anchorEl.setAttribute("hidden", "");
|
|
43000
|
-
setTopOffset(childEl, 0);
|
|
43001
|
-
return;
|
|
43002
|
-
}
|
|
43003
|
-
anchorEl.removeAttribute("hidden");
|
|
43004
|
-
const topOffset = computeTopOffset({
|
|
43005
|
-
anchorEl,
|
|
43006
|
-
childEl,
|
|
43007
|
-
textAnchor
|
|
43008
|
-
});
|
|
43009
|
-
setTopOffset(childEl, topOffset);
|
|
43010
|
-
}, [textAnchor, textKey, textSize, lineLayout?.size, lineLayout?.verticalAlign]);
|
|
43011
|
-
return jsxs(Fragment$1, {
|
|
43012
|
-
children: [children, jsx("span", {
|
|
43013
|
-
ref: anchorRef,
|
|
43014
|
-
className: "navi_text_anchor",
|
|
43015
|
-
"aria-hidden": "true",
|
|
43016
|
-
children: "\u200B"
|
|
43017
|
-
})]
|
|
43018
|
-
});
|
|
43019
|
-
};
|
|
43020
|
-
const setTopOffset = (childEl, topOffset) => {
|
|
43021
|
-
// position:relative + top shifts the element visually.
|
|
43022
|
-
// marginTop: -topOffset makes the layout box follow the visual position, so any container
|
|
43023
|
-
// (button, link, box…) computes its own padding/border/height based on the real final position
|
|
43024
|
-
// rather than the original unshifted one. This means a badge inside a button will symmetrically
|
|
43025
|
-
// expand the button height instead of overflowing or being clipped.
|
|
43026
|
-
// marginBottom: topOffset compensates the marginTop so the line height stays unchanged —
|
|
43027
|
-
// the shift is purely a repositioning, not an inflation of the line.
|
|
43028
|
-
if (!topOffset) {
|
|
43029
|
-
childEl.style.position = "";
|
|
43030
|
-
childEl.style.top = "";
|
|
43031
|
-
childEl.style.marginTop = "";
|
|
43032
|
-
childEl.style.marginBottom = "";
|
|
43033
|
-
return;
|
|
43034
|
-
}
|
|
43035
|
-
childEl.style.position = "relative";
|
|
43036
|
-
childEl.style.top = `${topOffset}px`;
|
|
43037
|
-
childEl.style.marginTop = `${-topOffset}px`;
|
|
43038
|
-
childEl.style.marginBottom = `${topOffset}px`;
|
|
43039
|
-
};
|
|
43040
|
-
const computeTopOffset = ({
|
|
43041
|
-
anchorEl,
|
|
43042
|
-
childEl,
|
|
43043
|
-
textAnchor
|
|
43044
|
-
}) => {
|
|
43045
|
-
if (textAnchor === "char-bottom") {
|
|
43046
|
-
// Align child's bottom with the char's bottom = the baseline.
|
|
43047
|
-
// The CSS spec says an inline-block with no text content has its baseline at its bottom margin edge.
|
|
43048
|
-
// So the browser's default placement already puts the child's bottom at the line's baseline.
|
|
43049
|
-
// No correction needed.
|
|
43050
|
-
return 0;
|
|
43051
|
-
}
|
|
43052
|
-
// The anchor's rendered rect corresponds to the surrounding text's line box:
|
|
43053
|
-
// top and bottom are the visual bounds of the line (including line-height).
|
|
43054
|
-
const anchorRect = anchorEl.getBoundingClientRect();
|
|
43055
|
-
|
|
43056
|
-
// Measure the child's current rect, then subtract any previously applied top correction
|
|
43057
|
-
// to recover its natural position — avoiding a style reset + reflow.
|
|
43058
|
-
const childRect = childEl.getBoundingClientRect();
|
|
43059
|
-
const childH = childRect.height;
|
|
43060
|
-
const previousTop = parseFloat(childEl.style.top) || 0;
|
|
43061
|
-
const childNaturalTop = childRect.top - previousTop;
|
|
43062
|
-
|
|
43063
|
-
// Compute desired child top Y based on textAnchor intention.
|
|
43064
|
-
let desiredChildTopY = 0;
|
|
43065
|
-
if (textAnchor === "line-top") {
|
|
43066
|
-
desiredChildTopY = anchorRect.top;
|
|
43067
|
-
} else if (textAnchor === "char-top") {
|
|
43068
|
-
const anchorStyle = getComputedStyle(anchorEl);
|
|
43069
|
-
const ctx = charTopCanvas.getContext("2d");
|
|
43070
|
-
ctx.font = `${anchorStyle.fontWeight} ${anchorStyle.fontSize} ${anchorStyle.fontFamily}`;
|
|
43071
|
-
const m = ctx.measureText("M");
|
|
43072
|
-
const baselineY = anchorRect.bottom - m.fontBoundingBoxDescent;
|
|
43073
|
-
desiredChildTopY = baselineY - m.actualBoundingBoxAscent;
|
|
43074
|
-
} else if (textAnchor === "center") {
|
|
43075
|
-
const anchorCenterY = (anchorRect.top + anchorRect.bottom) / 2;
|
|
43076
|
-
desiredChildTopY = anchorCenterY - childH / 2;
|
|
43077
|
-
} else if (textAnchor === "char-bottom") {
|
|
43078
|
-
// Already handled above (early return 0), but guard here for completeness.
|
|
43079
|
-
return 0;
|
|
43080
|
-
} else if (textAnchor === "line-bottom") {
|
|
43081
|
-
desiredChildTopY = anchorRect.bottom - childH;
|
|
43082
|
-
} else {
|
|
43083
|
-
return 0; // unknown textAnchor, no correction
|
|
43084
|
-
}
|
|
43085
|
-
return desiredChildTopY - childNaturalTop;
|
|
43086
|
-
};
|
|
43087
|
-
const charTopCanvas = document.createElement("canvas");
|
|
43088
|
-
|
|
43089
|
-
installImportMetaCssBuild(import.meta);const css$T = /* css */`
|
|
43090
|
-
@layer navi {
|
|
43091
|
-
/* Ensure data attributes from box.jsx can win to update display */
|
|
43092
|
-
.navi_icon {
|
|
43093
|
-
display: inline-flex;
|
|
43094
|
-
box-sizing: border-box;
|
|
43095
|
-
max-width: 100%;
|
|
43096
|
-
/* An icon never grows past the box it sits in, so a glyph can never make
|
|
43097
|
-
a line of text taller than the text itself. lineOverflow="allow" opts
|
|
43098
|
-
out, for an icon that is an affordance rather than a character — a
|
|
43099
|
-
control's chevron or clear button, sized to be touched, not read. */
|
|
43100
|
-
max-height: 100%;
|
|
43101
|
-
|
|
43102
|
-
&[data-line-overflow="allow"] {
|
|
43103
|
-
max-height: none;
|
|
43104
|
-
}
|
|
43105
|
-
}
|
|
43106
|
-
}
|
|
43107
|
-
|
|
43108
|
-
.navi_icon {
|
|
43109
|
-
white-space: nowrap;
|
|
43110
|
-
vertical-align: inherit;
|
|
43111
|
-
|
|
43112
|
-
&[data-icon-char] {
|
|
43113
|
-
aspect-ratio: 1/1;
|
|
43114
|
-
min-width: 0;
|
|
43115
|
-
height: round(1em, 1px);
|
|
43116
|
-
max-height: round(1em, 1px);
|
|
43117
|
-
flex-grow: 0 !important;
|
|
43118
|
-
align-items: center;
|
|
43119
|
-
justify-content: center;
|
|
43120
|
-
|
|
43121
|
-
/* fillLine: measured on the line box (1lh) instead of the character box
|
|
43122
|
-
(1em). The icon still stays inside the line — it just uses all of it,
|
|
43123
|
-
which is what an icon standing on its own in a control's slot wants,
|
|
43124
|
-
where a glyph sitting among letters wants to match their size. */
|
|
43125
|
-
&[data-fill-line] {
|
|
43126
|
-
height: round(1lh, 1px);
|
|
43127
|
-
max-height: round(1lh, 1px);
|
|
43128
|
-
}
|
|
43129
|
-
|
|
43130
|
-
svg,
|
|
43131
|
-
img {
|
|
43132
|
-
width: 100%;
|
|
43133
|
-
height: 100%;
|
|
43134
|
-
}
|
|
43135
|
-
svg {
|
|
43136
|
-
overflow: visible;
|
|
43137
|
-
}
|
|
43138
|
-
}
|
|
43139
|
-
&[data-flow-inline] {
|
|
43140
|
-
width: 1em;
|
|
43141
|
-
height: 1em;
|
|
43142
|
-
}
|
|
43143
|
-
&[data-interactive] {
|
|
43144
|
-
cursor: pointer;
|
|
43145
|
-
}
|
|
43146
|
-
&[data-icon-text] {
|
|
43147
|
-
-webkit-font-smoothing: antialiased;
|
|
43148
|
-
text-rendering: optimizeLegibility;
|
|
43149
|
-
}
|
|
43150
|
-
}
|
|
43151
|
-
|
|
43152
|
-
.navi_icon > svg,
|
|
43153
|
-
.navi_icon > img {
|
|
43154
|
-
width: 100%;
|
|
43155
|
-
height: 100%;
|
|
43156
|
-
}
|
|
43157
|
-
.navi_icon[data-width-fixed] > svg,
|
|
43158
|
-
.navi_icon[data-width-fixed] > img {
|
|
43159
|
-
width: 100%;
|
|
43160
|
-
height: auto;
|
|
43161
|
-
}
|
|
43162
|
-
.navi_icon[data-height-fixed] > svg,
|
|
43163
|
-
.navi_icon[data-height-fixed] > img {
|
|
43164
|
-
width: auto;
|
|
43165
|
-
height: 100%;
|
|
43166
|
-
}
|
|
43167
|
-
.navi_icon[data-width-fixed][data-height-fixed] > svg,
|
|
43168
|
-
.navi_icon[data-width-fixed][data-height-fixed] > img {
|
|
43169
|
-
width: 100%;
|
|
43170
|
-
height: 100%;
|
|
43171
|
-
}
|
|
43172
|
-
`;
|
|
43173
|
-
|
|
43174
|
-
/**
|
|
43175
|
-
* Renders an icon — an inline SVG/emoji/text glyph that inherits the
|
|
43176
|
-
* surrounding text's `currentColor` and (by default) its font size, so it sits
|
|
43177
|
-
* on the text baseline like a character.
|
|
43178
|
-
*
|
|
43179
|
-
* Content comes from either `href` (references an external/sprite symbol via
|
|
43180
|
-
* `<use>`) or `children` (an inline `<svg>` element, or a string for a
|
|
43181
|
-
* text/emoji icon). All extra props are spread onto the underlying `Box`/`Text`
|
|
43182
|
-
* (sizing, spacing, color, className, data-attributes, …).
|
|
43183
|
-
*
|
|
43184
|
-
* Render mode is chosen automatically:
|
|
43185
|
-
* - `children` is a **string** → a text icon (`<Text data-icon-text>`).
|
|
43186
|
-
* - **sized** (an explicit `width`/`height`, or `flex`/`grid`) → a block icon
|
|
43187
|
-
* (`<Box square>`), laid out as its own box rather than inline.
|
|
43188
|
-
* - otherwise → an **inline char-like** icon that flows on the text baseline
|
|
43189
|
-
* (`data-icon-char`), aligned via `textAnchor`.
|
|
43190
|
-
*
|
|
43191
|
-
* Accessibility: an icon is treated as decorative (`aria-hidden`) by default
|
|
43192
|
-
* whenever it has no explicit size and no `onClick`; give it an explicit
|
|
43193
|
-
* `decorative={false}` (or make it interactive) when it conveys meaning.
|
|
43194
|
-
*
|
|
43195
|
-
* @param {object} props
|
|
43196
|
-
* @param {string} [props.href] - URL/id of an external SVG symbol, rendered via
|
|
43197
|
-
* `<svg><use href></svg>`. Mutually exclusive with meaningful `children`.
|
|
43198
|
-
* @param {import("ignore:preact").ComponentChildren} [props.children] - Inline icon
|
|
43199
|
-
* content: an `<svg>` element, or a string (renders as a text/emoji icon).
|
|
43200
|
-
* @param {boolean} [props.decorative] - Marks the icon `aria-hidden`. Defaults
|
|
43201
|
-
* to `true` for an unsized, non-interactive icon; pass `false` for a
|
|
43202
|
-
* meaning-bearing icon that needs to be exposed to assistive tech.
|
|
43203
|
-
* @param {(event: MouseEvent) => void} [props.onClick] - Makes the icon
|
|
43204
|
-
* interactive (`data-interactive`, pointer cursor) and non-decorative.
|
|
43205
|
-
* @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [props.textAnchor="center"]
|
|
43206
|
-
* - Vertical alignment within the surrounding text line for the inline
|
|
43207
|
-
* char-like mode, forwarded to `TextAnchor`: `"line-top"`/`"line-bottom"`
|
|
43208
|
-
* align to the line box edges, `"char-top"` to the ink ascent, `"center"`
|
|
43209
|
-
* centers on the line box, `"char-bottom"` sits on the baseline. See
|
|
43210
|
-
* `text_anchor.jsx`.
|
|
43211
|
-
* @param {{ size?: number, verticalAlign?: string }} [props.lineLayout] -
|
|
43212
|
-
* Describes the surrounding line context (font size / vertical-align),
|
|
43213
|
-
* forwarded to `TextAnchor` so it recomputes the vertical correction when
|
|
43214
|
-
* that context changes.
|
|
43215
|
-
* @param {string|number} [props.width] - Explicit width; `"auto"` clears it.
|
|
43216
|
-
* Any explicit size switches the icon to block (sized) mode.
|
|
43217
|
-
* @param {string|number} [props.height] - Explicit height; `"auto"` clears it.
|
|
43218
|
-
* @param {"allow"} [props.lineOverflow] - `"allow"` lets the icon be taller
|
|
43219
|
-
* than the box it sits in (a line of text, a control's slot) instead of being
|
|
43220
|
-
* capped by it. For an icon that is an affordance sized for the finger rather
|
|
43221
|
-
* than a character sized for reading.
|
|
43222
|
-
* @param {boolean} [props.fillLine] - Sizes the icon on the line box (1lh)
|
|
43223
|
-
* rather than on the character box (1em), so it uses the full height of the
|
|
43224
|
-
* line without leaving it. Unlike `lineOverflow`, the icon still never
|
|
43225
|
-
* exceeds the line.
|
|
43226
|
-
* @param {boolean} [props.square] - Keep a 1:1 box; combined with one explicit
|
|
43227
|
-
* dimension it fixes the other too.
|
|
43228
|
-
* @param {boolean} [props.circle] - Like `square`, plus a circular shape.
|
|
43229
|
-
* @param {string|number} [props.aspectRatio] - Fixes the second dimension from
|
|
43230
|
-
* the one explicit dimension.
|
|
43231
|
-
* @param {"x"|"y"|boolean} [props.flex] - Forces block/flex layout; auto-set to
|
|
43232
|
-
* `"x"` when the icon is sized.
|
|
43233
|
-
* @param {boolean} [props.grid] - Forces block/grid layout.
|
|
43234
|
-
* @param {string} [props.className] - Merged with the base `"navi_icon"` class.
|
|
43235
|
-
*/
|
|
43236
|
-
const Icon = ({
|
|
43237
|
-
href,
|
|
43238
|
-
children,
|
|
43239
|
-
decorative,
|
|
43240
|
-
onClick,
|
|
43241
|
-
textAnchor = "center",
|
|
43242
|
-
lineLayout,
|
|
43243
|
-
lineOverflow,
|
|
43244
|
-
fillLine,
|
|
43245
|
-
...props
|
|
43246
|
-
}) => {
|
|
43247
|
-
import.meta.css = [css$T, "@jsenv/navi/src/text/icon.jsx"];
|
|
43248
|
-
const innerChildren = href ? jsx("svg", {
|
|
43249
|
-
width: "100%",
|
|
43250
|
-
height: "100%",
|
|
43251
|
-
children: jsx("use", {
|
|
43252
|
-
href: href
|
|
43253
|
-
})
|
|
43254
|
-
}) : children;
|
|
43255
|
-
let {
|
|
43256
|
-
flex,
|
|
43257
|
-
grid,
|
|
43258
|
-
width,
|
|
43259
|
-
height
|
|
43260
|
-
} = props;
|
|
43261
|
-
if (width === "auto") {
|
|
43262
|
-
width = undefined;
|
|
43263
|
-
}
|
|
43264
|
-
if (height === "auto") {
|
|
43265
|
-
height = undefined;
|
|
43266
|
-
}
|
|
43267
|
-
const hasExplicitWidth = width !== undefined;
|
|
43268
|
-
const hasExplicitHeight = height !== undefined;
|
|
43269
|
-
const widthFixed = hasExplicitWidth || hasExplicitHeight && (props.square || props.circle || props.aspectRatio);
|
|
43270
|
-
const heightFixed = hasExplicitHeight || hasExplicitWidth && (props.square || props.circle || props.aspectRatio);
|
|
43271
|
-
if (widthFixed || heightFixed) {
|
|
43272
|
-
if (flex === undefined) {
|
|
43273
|
-
flex = "x";
|
|
43274
|
-
}
|
|
43275
|
-
} else if (decorative === undefined && !onClick) {
|
|
43276
|
-
decorative = true;
|
|
43277
|
-
}
|
|
43278
|
-
const ariaProps = decorative ? {
|
|
43279
|
-
"aria-hidden": "true"
|
|
43280
|
-
} : {};
|
|
43281
|
-
const textRef = useRef();
|
|
43282
|
-
if (typeof children === "string") {
|
|
43283
|
-
return jsx(Text, {
|
|
43284
|
-
...props,
|
|
43285
|
-
...ariaProps,
|
|
43286
|
-
"data-icon-text": "",
|
|
43287
|
-
"data-line-overflow": lineOverflow,
|
|
43288
|
-
"data-fill-line": fillLine ? "" : undefined,
|
|
43289
|
-
children: children
|
|
43290
|
-
});
|
|
43291
|
-
}
|
|
43292
|
-
if (flex || grid) {
|
|
43293
|
-
return jsx(Box, {
|
|
43294
|
-
square: true,
|
|
43295
|
-
...props,
|
|
43296
|
-
...ariaProps,
|
|
43297
|
-
flex: flex,
|
|
43298
|
-
baseClassName: "navi_icon",
|
|
43299
|
-
"data-width-fixed": widthFixed ? "" : undefined,
|
|
43300
|
-
"data-height-fixed": heightFixed ? "" : undefined,
|
|
43301
|
-
"data-interactive": onClick ? "" : undefined,
|
|
43302
|
-
"data-line-overflow": lineOverflow,
|
|
43303
|
-
"data-fill-line": fillLine ? "" : undefined,
|
|
43304
|
-
onClick: onClick,
|
|
43305
|
-
children: innerChildren
|
|
43306
|
-
});
|
|
43307
|
-
}
|
|
43308
|
-
return jsx(TextAnchor, {
|
|
43309
|
-
childRef: textRef,
|
|
43310
|
-
textAnchor: textAnchor,
|
|
43311
|
-
textSize: props.size,
|
|
43312
|
-
lineLayout: lineLayout,
|
|
43313
|
-
children: jsxs(Text, {
|
|
43314
|
-
...props,
|
|
43315
|
-
...ariaProps,
|
|
43316
|
-
className: withPropsClassName("navi_icon", props.className),
|
|
43317
|
-
spacing: "pre",
|
|
43318
|
-
"data-icon-char": "",
|
|
43319
|
-
"data-line-overflow": lineOverflow,
|
|
43320
|
-
"data-fill-line": fillLine ? "" : undefined,
|
|
43321
|
-
"data-width-fixed": widthFixed ? "" : undefined,
|
|
43322
|
-
"data-height-fixed": heightFixed ? "" : undefined,
|
|
43323
|
-
"data-interactive": onClick ? "" : undefined,
|
|
43324
|
-
onClick: onClick,
|
|
43325
|
-
ref: textRef,
|
|
43326
|
-
children: [jsx("span", {
|
|
43327
|
-
style: "user-select:none",
|
|
43328
|
-
children: "\u200B"
|
|
43329
|
-
}), innerChildren]
|
|
43330
|
-
})
|
|
43331
|
-
});
|
|
43332
|
-
};
|
|
43333
|
-
|
|
43334
43698
|
/**
|
|
43335
43699
|
* Hook that reactively checks if a URL is visited.
|
|
43336
43700
|
* Re-renders when the visited URL set changes.
|
|
@@ -55463,6 +55827,16 @@ const isTextInputElement = (el) => {
|
|
|
55463
55827
|
);
|
|
55464
55828
|
};
|
|
55465
55829
|
|
|
55830
|
+
// How many lines the thing around a component gives it, when that thing caps
|
|
55831
|
+
// its own height and cannot cap what it holds.
|
|
55832
|
+
//
|
|
55833
|
+
// A <Picker> is the case it exists for: its value is clamped with maxLines,
|
|
55834
|
+
// which is CSS line-clamp — it counts line boxes of inline text and has no idea
|
|
55835
|
+
// what a wrapped flex row is. So a <BadgeList> rendered as a picker's ui reads
|
|
55836
|
+
// the number from here and caps its own rows to it, and the picker turns its
|
|
55837
|
+
// own clamp off (see .navi_picker_value:has(.navi_badge_list) in picker.jsx).
|
|
55838
|
+
const MaxLinesContext = createContext(undefined);
|
|
55839
|
+
|
|
55466
55840
|
// When a component render a prop that can be anything (js value of preact element)
|
|
55467
55841
|
// 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
55842
|
const renderSafe = (value) => {
|
|
@@ -57861,12 +58235,24 @@ const ListItemSelectable = props => {
|
|
|
57861
58235
|
...rest
|
|
57862
58236
|
} = props;
|
|
57863
58237
|
const multiple = useContext(SelectableListMultipleContext);
|
|
58238
|
+
// Whose reason it is that this row cannot be taken. Read-only reaching it
|
|
58239
|
+
// from above is the LIST's, and what is settled is then the whole answer —
|
|
58240
|
+
// said as the selection where several things are taken, as the choice where
|
|
58241
|
+
// one thing is. Said of each row in turn, "this option is not available"
|
|
58242
|
+
// describes something else entirely: a list where each row happens to be
|
|
58243
|
+
// unavailable for its own reasons, which goes on being said that way. Busy
|
|
58244
|
+
// is not settled either: a list waiting on something says nothing about what
|
|
58245
|
+
// will be possible once it is done, so the row keeps its own words.
|
|
58246
|
+
const readOnlyFromAbove = useContext(ReadOnlyContext);
|
|
58247
|
+
const loadingFromAbove = useContext(LoadingContext$1);
|
|
58248
|
+
const answerIsSettled = Boolean(readOnlyFromAbove) && !loadingFromAbove;
|
|
58249
|
+
const readOnlyMessageKey = answerIsSettled ? multiple ? `constraint.readonly.selection` : `constraint.readonly.choice` : `constraint.readonly.option`;
|
|
57864
58250
|
const inputRef = useRef();
|
|
57865
58251
|
const inputType = multiple ? "checkbox" : "radio";
|
|
57866
58252
|
const inputId = `${id}_input`;
|
|
57867
58253
|
inputRef.nullCanHappen = true; // virtualization
|
|
57868
58254
|
const [checkableRootProps, checkableProps, controlChildrenWrapperProps] = useCheckableProps({
|
|
57869
|
-
readOnlyMessage: naviI18n(
|
|
58255
|
+
readOnlyMessage: naviI18n(readOnlyMessageKey, props),
|
|
57870
58256
|
...rest,
|
|
57871
58257
|
ref: inputRef,
|
|
57872
58258
|
id: inputId,
|
|
@@ -62511,6 +62897,51 @@ const PickerPresetResolver = props => {
|
|
|
62511
62897
|
});
|
|
62512
62898
|
};
|
|
62513
62899
|
|
|
62900
|
+
// Put around its children by BadgeList so a Badge below knows it is inside one.
|
|
62901
|
+
// A badge then renders nothing at all: it hands its props to the list and the
|
|
62902
|
+
// list renders it. Badges register in tree order, so by the time the list gets
|
|
62903
|
+
// to its own content it holds them all, in source order, and knows how many
|
|
62904
|
+
// there are before deciding what to show — without ever walking children
|
|
62905
|
+
// vnodes, and without rendering a badge it then has to take back.
|
|
62906
|
+
const BadgeListContext = createContext(null);
|
|
62907
|
+
|
|
62908
|
+
const createBadgeRegistry = () => {
|
|
62909
|
+
let pass = 0;
|
|
62910
|
+
let passCount = 0;
|
|
62911
|
+
let entries = [];
|
|
62912
|
+
let childrenAreNew = true;
|
|
62913
|
+
|
|
62914
|
+
return {
|
|
62915
|
+
// Called by BadgeList at the top of every render. childrenAreNew says
|
|
62916
|
+
// whether it was handed fresh children vnodes: when it re-renders on its
|
|
62917
|
+
// own (its own state changed) Preact hands the untouched children straight
|
|
62918
|
+
// back and skips them, so none of the badges registers again. That empty
|
|
62919
|
+
// pass means "unchanged", not "no badge left" — see getEntries.
|
|
62920
|
+
startPass: (areNew) => {
|
|
62921
|
+
pass++;
|
|
62922
|
+
passCount = 0;
|
|
62923
|
+
childrenAreNew = areNew;
|
|
62924
|
+
},
|
|
62925
|
+
// entryState is the badge's own memory. A badge that re-renders on its own
|
|
62926
|
+
// must update its entry, not append a second one.
|
|
62927
|
+
register: (entryState, props) => {
|
|
62928
|
+
if (entryState.pass !== pass) {
|
|
62929
|
+
entryState.pass = pass;
|
|
62930
|
+
entryState.index = passCount;
|
|
62931
|
+
passCount++;
|
|
62932
|
+
}
|
|
62933
|
+
entries[entryState.index] = props;
|
|
62934
|
+
},
|
|
62935
|
+
getEntries: () => {
|
|
62936
|
+
if (passCount === 0 && !childrenAreNew) {
|
|
62937
|
+
return entries;
|
|
62938
|
+
}
|
|
62939
|
+
entries.length = passCount;
|
|
62940
|
+
return entries;
|
|
62941
|
+
},
|
|
62942
|
+
};
|
|
62943
|
+
};
|
|
62944
|
+
|
|
62514
62945
|
installImportMetaCssBuild(import.meta);const css$x = /* css */`
|
|
62515
62946
|
@layer navi {
|
|
62516
62947
|
}
|
|
@@ -62569,6 +63000,11 @@ installImportMetaCssBuild(import.meta);const css$x = /* css */`
|
|
|
62569
63000
|
align-items: stretch;
|
|
62570
63001
|
color: var(--x-color);
|
|
62571
63002
|
font-size: var(--font-size);
|
|
63003
|
+
/* Cuts the font's half-leading above the first line and below the last one,
|
|
63004
|
+
down to cap-height/baseline: the padding becomes the only vertical space
|
|
63005
|
+
and the text is exactly centered. Space between wrapped lines is left
|
|
63006
|
+
untouched, unlike a line-height tweak. */
|
|
63007
|
+
text-box: trim-both cap alphabetic;
|
|
62572
63008
|
background: var(--x-background);
|
|
62573
63009
|
background-color: var(--x-background-color);
|
|
62574
63010
|
border-radius: 1em;
|
|
@@ -62618,7 +63054,22 @@ installImportMetaCssBuild(import.meta);const css$x = /* css */`
|
|
|
62618
63054
|
}
|
|
62619
63055
|
}
|
|
62620
63056
|
`;
|
|
62621
|
-
const Badge =
|
|
63057
|
+
const Badge = props => {
|
|
63058
|
+
const badgeList = useContext(BadgeListContext);
|
|
63059
|
+
const entryStateRef = useRef();
|
|
63060
|
+
if (badgeList) {
|
|
63061
|
+
// Inside a BadgeList the badge renders nothing: it hands itself over and
|
|
63062
|
+
// the list renders it, which is how the list gets to see them all before
|
|
63063
|
+
// deciding how many it shows. See badge_list_context.js.
|
|
63064
|
+
const entryState = entryStateRef.current || (entryStateRef.current = {});
|
|
63065
|
+
badgeList.register(entryState, props);
|
|
63066
|
+
return null;
|
|
63067
|
+
}
|
|
63068
|
+
return jsx(BadgeUI, {
|
|
63069
|
+
...props
|
|
63070
|
+
});
|
|
63071
|
+
};
|
|
63072
|
+
const BadgeUI = ({
|
|
62622
63073
|
children,
|
|
62623
63074
|
className,
|
|
62624
63075
|
...props
|
|
@@ -62633,7 +63084,14 @@ const Badge = ({
|
|
|
62633
63084
|
return jsx(Text, {
|
|
62634
63085
|
className: withPropsClassName("navi_badge", className),
|
|
62635
63086
|
bold: true,
|
|
62636
|
-
maxLines: 1
|
|
63087
|
+
maxLines: 1
|
|
63088
|
+
// The text-box trim ends the content box at the baseline, and a clamped
|
|
63089
|
+
// badge is clipped there: descenders of the last visible line would be
|
|
63090
|
+
// cut. Halfway to the next line's cap top keeps them, still above any
|
|
63091
|
+
// ink from the line the clamp hides.
|
|
63092
|
+
,
|
|
63093
|
+
|
|
63094
|
+
overflowClipMargin: "content-box calc((1lh - 1cap) / 2)",
|
|
62637
63095
|
...props,
|
|
62638
63096
|
styleCSSVars: BadgeStyleCSSVars,
|
|
62639
63097
|
spacing: jsx("span", {}),
|
|
@@ -62657,6 +63115,15 @@ const BadgeStyleCSSVars = {
|
|
|
62657
63115
|
fontSize: "--font-size"
|
|
62658
63116
|
};
|
|
62659
63117
|
const BadgeButton = props => {
|
|
63118
|
+
const ownTargetHidden = useOwnTargetHidden(props);
|
|
63119
|
+
if (ownTargetHidden) {
|
|
63120
|
+
return null;
|
|
63121
|
+
}
|
|
63122
|
+
return jsx(BadgeButtonUI, {
|
|
63123
|
+
...props
|
|
63124
|
+
});
|
|
63125
|
+
};
|
|
63126
|
+
const BadgeButtonUI = props => {
|
|
62660
63127
|
const defaultRef = useRef();
|
|
62661
63128
|
props.ref = props.ref || defaultRef;
|
|
62662
63129
|
const [buttonRootProps, buttonHostProps] = useControlProps(props, {
|
|
@@ -62687,20 +63154,208 @@ installImportMetaCssBuild(import.meta);const css$w = /* css */`
|
|
|
62687
63154
|
visibility: hidden;
|
|
62688
63155
|
pointer-events: none;
|
|
62689
63156
|
}
|
|
63157
|
+
|
|
63158
|
+
/* maxLines renders every badge for one layout, reads where the rows fell,
|
|
63159
|
+
then renders again with only what fits. The in-between is hidden rather
|
|
63160
|
+
than clipped: the badges that don't make it must leave the DOM, not sit
|
|
63161
|
+
there cut in half. Both renders land in the same frame (the second one
|
|
63162
|
+
is queued from a layout effect), so nothing shows up half measured. */
|
|
63163
|
+
&[navi-badge-list-measuring] {
|
|
63164
|
+
visibility: hidden;
|
|
63165
|
+
}
|
|
62690
63166
|
}
|
|
62691
63167
|
|
|
62692
63168
|
.navi_badge.navi_badge_more {
|
|
62693
63169
|
white-space: nowrap;
|
|
62694
63170
|
}
|
|
62695
63171
|
`;
|
|
62696
|
-
|
|
63172
|
+
|
|
63173
|
+
// Groups badges by the row they wrapped onto.
|
|
63174
|
+
// The signal we look for is horizontal: inside a row each badge starts further
|
|
63175
|
+
// right than the previous one, at a wrap the next one starts back at the row
|
|
63176
|
+
// start. Vertical positions can't be used because "align-items: center" gives
|
|
63177
|
+
// badges of different heights different tops within a single row.
|
|
63178
|
+
const groupRectsByRow = elements => {
|
|
63179
|
+
const rows = [];
|
|
63180
|
+
let previousLeft = -Infinity;
|
|
63181
|
+
for (const element of elements) {
|
|
63182
|
+
const rect = element.getBoundingClientRect();
|
|
63183
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
63184
|
+
continue;
|
|
63185
|
+
}
|
|
63186
|
+
if (rows.length === 0 || rect.left <= previousLeft) {
|
|
63187
|
+
rows.push([]);
|
|
63188
|
+
}
|
|
63189
|
+
rows[rows.length - 1].push(rect);
|
|
63190
|
+
previousLeft = rect.left;
|
|
63191
|
+
}
|
|
63192
|
+
return rows;
|
|
63193
|
+
};
|
|
63194
|
+
|
|
63195
|
+
// Reads a list that currently holds every badge plus the "+N" badge and tells
|
|
63196
|
+
// how many badges fit in maxLines rows.
|
|
63197
|
+
const measureRowFit = (listEl, maxLines) => {
|
|
63198
|
+
const elements = Array.from(listEl.children);
|
|
63199
|
+
if (elements.length < 2) {
|
|
63200
|
+
return elements.length;
|
|
63201
|
+
}
|
|
63202
|
+
// The "+N" badge is rendered last. It sits after every badge so it moves none
|
|
63203
|
+
// of them, which is what lets it be measured in the same layout it is left
|
|
63204
|
+
// out of.
|
|
63205
|
+
const moreRect = elements[elements.length - 1].getBoundingClientRect();
|
|
63206
|
+
const badgeElements = elements.slice(0, -1);
|
|
63207
|
+
const rows = groupRectsByRow(badgeElements);
|
|
63208
|
+
if (rows.length <= maxLines) {
|
|
63209
|
+
return badgeElements.length;
|
|
63210
|
+
}
|
|
63211
|
+
const styles = getComputedStyle(listEl);
|
|
63212
|
+
const gap = parseFloat(styles.columnGap) || 0;
|
|
63213
|
+
const contentRight = listEl.getBoundingClientRect().right - (parseFloat(styles.paddingRight) || 0) - (parseFloat(styles.borderRightWidth) || 0);
|
|
63214
|
+
|
|
63215
|
+
// The "+N" badge lands right after the last kept badge, so it eats into the
|
|
63216
|
+
// last visible row: drop badges from that row until it fits. Emptying that
|
|
63217
|
+
// row entirely is a valid outcome — the badge then wraps onto it and takes it
|
|
63218
|
+
// for itself, which is still within maxLines.
|
|
63219
|
+
const rowsKept = rows.slice(0, maxLines);
|
|
63220
|
+
const lastRow = rowsKept[rowsKept.length - 1];
|
|
63221
|
+
let lastRowCount = lastRow.length;
|
|
63222
|
+
while (lastRowCount > 0 && lastRow[lastRowCount - 1].right + gap + moreRect.width > contentRight + 0.5) {
|
|
63223
|
+
lastRowCount--;
|
|
63224
|
+
}
|
|
63225
|
+
return rowsKept.slice(0, -1).reduce((count, row) => count + row.length, 0) + lastRowCount;
|
|
63226
|
+
};
|
|
63227
|
+
const BADGE_LIST_PROPS = {
|
|
63228
|
+
inline: true,
|
|
63229
|
+
flex: "x",
|
|
63230
|
+
alignY: "center",
|
|
63231
|
+
spacing: "xs"
|
|
63232
|
+
};
|
|
63233
|
+
|
|
63234
|
+
// The badges hand themselves over as they render instead of being read upfront
|
|
63235
|
+
// from the children vnodes: see badge_list_context.js. Only worth it when the
|
|
63236
|
+
// list has something to decide — how many to show, or whether there is none at
|
|
63237
|
+
// all. Otherwise the badges render themselves and nothing has to be collected.
|
|
63238
|
+
const useBadgeRegistry = (children, enabled) => {
|
|
63239
|
+
const registryRef = useRef();
|
|
63240
|
+
const previousChildrenRef = useRef();
|
|
63241
|
+
if (!enabled) {
|
|
63242
|
+
return null;
|
|
63243
|
+
}
|
|
63244
|
+
const registry = registryRef.current || (registryRef.current = createBadgeRegistry());
|
|
63245
|
+
// Whether Preact is about to render the badges again or hand back the ones it
|
|
63246
|
+
// already has, which it does when the list re-renders on its own.
|
|
63247
|
+
registry.startPass(previousChildrenRef.current !== children);
|
|
63248
|
+
previousChildrenRef.current = children;
|
|
63249
|
+
return registry;
|
|
63250
|
+
};
|
|
63251
|
+
|
|
63252
|
+
/**
|
|
63253
|
+
* A row of badges that wraps.
|
|
63254
|
+
*
|
|
63255
|
+
* Four shapes behind one name, because what the list has to do at runtime is
|
|
63256
|
+
* not the same in each. Nothing is set up for a case that cannot happen: a
|
|
63257
|
+
* plain list is one element holding its children as-is — no registry, no
|
|
63258
|
+
* effect —, a capped one collects its badges but measures nothing, and only
|
|
63259
|
+
* shrinkWrap ever builds the measurement ghost.
|
|
63260
|
+
*
|
|
63261
|
+
* @param {import("ignore:preact").ComponentChildren} [fallback]
|
|
63262
|
+
* Rendered in place of the badges when there is none. Without it an empty
|
|
63263
|
+
* list renders nothing at all. In a <Picker> this is the only placeholder
|
|
63264
|
+
* the user gets — a picker given a `ui` does not draw its own — so pass the
|
|
63265
|
+
* placeholder text: `fallback="Select skills…"`. Plain text is the right
|
|
63266
|
+
* choice there: it reads at the picker's size, in the placeholder color the
|
|
63267
|
+
* picker gives its empty value slot, and the box stays the same height. A
|
|
63268
|
+
* transparent <Badge> matches the slot to the pixel instead, at the price of
|
|
63269
|
+
* badge-sized text. See docs/badge_list.md.
|
|
63270
|
+
* @param {boolean} [shrinkWrap]
|
|
63271
|
+
* Narrows the list down to its widest row so the last row isn't ragged.
|
|
63272
|
+
* Defaults to true inside a <Picker> — the trigger draws a border around the
|
|
63273
|
+
* list, so the ragged edge shows — and false elsewhere, where the work would
|
|
63274
|
+
* often go unseen: opt in where an edge is visible. Ignored when maxLines is
|
|
63275
|
+
* in play, which needs the full width to know where the rows fall.
|
|
63276
|
+
* @param {number} [max]
|
|
63277
|
+
* Caps how many badges are rendered; the surplus becomes a "+N" badge, which
|
|
63278
|
+
* takes one of the max slots.
|
|
63279
|
+
* @param {number} [maxLines]
|
|
63280
|
+
* Caps how many rows are shown, measured. Falls back to what the surrounding
|
|
63281
|
+
* component grants (a <Picker>, see max_lines_context.js).
|
|
63282
|
+
*/
|
|
63283
|
+
const BadgeList = props => {
|
|
63284
|
+
import.meta.css = [css$w, "@jsenv/navi/src/text/badge_list.jsx"];
|
|
63285
|
+
const {
|
|
63286
|
+
maxLines,
|
|
63287
|
+
max,
|
|
63288
|
+
fallback
|
|
63289
|
+
} = props;
|
|
63290
|
+
const maxLinesFromAbove = useContext(MaxLinesContext);
|
|
63291
|
+
const maxLinesResolved = maxLines === undefined ? maxLinesFromAbove : maxLines;
|
|
63292
|
+
// Granted lines mean a clamping container — a <Picker> — whose border makes
|
|
63293
|
+
// the ragged last row show; nothing of the sort around us, and shrink
|
|
63294
|
+
// wrapping is work nobody sees unless asked for.
|
|
63295
|
+
const {
|
|
63296
|
+
shrinkWrap = maxLinesFromAbove !== undefined
|
|
63297
|
+
} = props;
|
|
63298
|
+
if (maxLinesResolved !== undefined) {
|
|
63299
|
+
// shrinkWrap is dropped on purpose: it narrows the list down to its widest
|
|
63300
|
+
// row, which would re-wrap the badges under the cap just measured.
|
|
63301
|
+
return jsx(BadgeListMaxLines, {
|
|
63302
|
+
...props,
|
|
63303
|
+
maxLines: maxLinesResolved
|
|
63304
|
+
});
|
|
63305
|
+
}
|
|
63306
|
+
if (shrinkWrap) {
|
|
63307
|
+
return jsx(BadgeListShrinkWrap, {
|
|
63308
|
+
...props
|
|
63309
|
+
});
|
|
63310
|
+
}
|
|
63311
|
+
if (max !== undefined || fallback !== undefined) {
|
|
63312
|
+
return jsx(BadgeListCounted, {
|
|
63313
|
+
...props
|
|
63314
|
+
});
|
|
63315
|
+
}
|
|
63316
|
+
return jsx(BadgeListPlain, {
|
|
63317
|
+
...props
|
|
63318
|
+
});
|
|
63319
|
+
};
|
|
63320
|
+
|
|
63321
|
+
// Nothing to measure, cap, or count: the badges render themselves — no
|
|
63322
|
+
// registry, no context, no effect, one element.
|
|
63323
|
+
const BadgeListPlain = props => {
|
|
63324
|
+
return jsx(Box, {
|
|
63325
|
+
baseClassName: "navi_badge_list",
|
|
63326
|
+
...BADGE_LIST_PROPS,
|
|
63327
|
+
...props
|
|
63328
|
+
});
|
|
63329
|
+
};
|
|
63330
|
+
|
|
63331
|
+
// max/fallback need the badge count, so the badges are collected — but nothing
|
|
63332
|
+
// is measured and no DOM is watched.
|
|
63333
|
+
const BadgeListCounted = ({
|
|
62697
63334
|
fallback,
|
|
62698
63335
|
children,
|
|
62699
|
-
shrinkWrap = true,
|
|
62700
63336
|
max,
|
|
62701
|
-
...
|
|
63337
|
+
...boxProps
|
|
62702
63338
|
}) => {
|
|
62703
|
-
|
|
63339
|
+
const registry = useBadgeRegistry(children, true);
|
|
63340
|
+
return jsx(Box, {
|
|
63341
|
+
baseClassName: "navi_badge_list",
|
|
63342
|
+
...BADGE_LIST_PROPS,
|
|
63343
|
+
...boxProps,
|
|
63344
|
+
children: jsx(BadgeListChildren, {
|
|
63345
|
+
registry: registry,
|
|
63346
|
+
max: max,
|
|
63347
|
+
fallback: fallback,
|
|
63348
|
+
children: children
|
|
63349
|
+
})
|
|
63350
|
+
});
|
|
63351
|
+
};
|
|
63352
|
+
const BadgeListShrinkWrap = ({
|
|
63353
|
+
fallback,
|
|
63354
|
+
children,
|
|
63355
|
+
max,
|
|
63356
|
+
...restProps
|
|
63357
|
+
}) => {
|
|
63358
|
+
const registry = useBadgeRegistry(children, max !== undefined || fallback !== undefined);
|
|
62704
63359
|
const measureRef = useRef();
|
|
62705
63360
|
const visibleRef = useRef();
|
|
62706
63361
|
useLayoutEffect(() => {
|
|
@@ -62713,18 +63368,25 @@ const BadgeList = ({
|
|
|
62713
63368
|
let rafId;
|
|
62714
63369
|
const measure = () => {
|
|
62715
63370
|
visibleEl.style.width = "";
|
|
62716
|
-
|
|
62717
|
-
|
|
62718
|
-
|
|
62719
|
-
|
|
62720
|
-
|
|
62721
|
-
|
|
62722
|
-
|
|
62723
|
-
|
|
62724
|
-
visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
|
|
62725
|
-
}
|
|
63371
|
+
// Clone the already-rendered DOM nodes instead of letting React/Preact
|
|
63372
|
+
// render the children a second time into the ghost: re-rendering would
|
|
63373
|
+
// instantiate Badge/Badge.Button a second time, double-registering their
|
|
63374
|
+
// controllers (and any other mount side effect) under the same id.
|
|
63375
|
+
measureEl.replaceChildren(...Array.from(visibleEl.children, child => child.cloneNode(true)));
|
|
63376
|
+
const optimalWidth = measureWidestChildRow(measureEl);
|
|
63377
|
+
if (optimalWidth !== null) {
|
|
63378
|
+
visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
|
|
62726
63379
|
}
|
|
62727
63380
|
};
|
|
63381
|
+
|
|
63382
|
+
// A single badge is a single row, and a single row is already the widest
|
|
63383
|
+
// one: there is nothing to narrow, and no width the list could be given
|
|
63384
|
+
// that would change that.
|
|
63385
|
+
if (visibleEl.children.length < 2) {
|
|
63386
|
+
visibleEl.style.width = "";
|
|
63387
|
+
measureEl.replaceChildren();
|
|
63388
|
+
return undefined;
|
|
63389
|
+
}
|
|
62728
63390
|
measure();
|
|
62729
63391
|
const onResize = () => {
|
|
62730
63392
|
cancelAnimationFrame(rafId);
|
|
@@ -62741,36 +63403,201 @@ const BadgeList = ({
|
|
|
62741
63403
|
observer?.disconnect();
|
|
62742
63404
|
window.removeEventListener("resize", onResize);
|
|
62743
63405
|
};
|
|
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
|
|
63406
|
+
}, [children]);
|
|
63407
|
+
const boxProps = {
|
|
63408
|
+
...BADGE_LIST_PROPS,
|
|
63409
|
+
...restProps
|
|
62755
63410
|
};
|
|
62756
|
-
return
|
|
62757
|
-
|
|
62758
|
-
|
|
62759
|
-
|
|
62760
|
-
|
|
62761
|
-
|
|
62762
|
-
|
|
62763
|
-
|
|
62764
|
-
|
|
62765
|
-
|
|
62766
|
-
|
|
62767
|
-
|
|
62768
|
-
|
|
62769
|
-
|
|
62770
|
-
|
|
62771
|
-
|
|
63411
|
+
return (
|
|
63412
|
+
// inline flex, not a plain block: the wrapper must sit on the line the way
|
|
63413
|
+
// the list itself would, otherwise the list lands a pixel low in some
|
|
63414
|
+
// containers.
|
|
63415
|
+
jsxs(Box, {
|
|
63416
|
+
relative: true,
|
|
63417
|
+
inline: true,
|
|
63418
|
+
flex: "x",
|
|
63419
|
+
children: [jsx(Box, {
|
|
63420
|
+
baseClassName: "navi_badge_list",
|
|
63421
|
+
...boxProps,
|
|
63422
|
+
ref: measureRef,
|
|
63423
|
+
"aria-hidden": "true",
|
|
63424
|
+
"navi-badge-list-clone": ""
|
|
63425
|
+
}), jsx(Box, {
|
|
63426
|
+
baseClassName: "navi_badge_list",
|
|
63427
|
+
...boxProps,
|
|
63428
|
+
ref: visibleRef,
|
|
63429
|
+
children: jsx(BadgeListChildren, {
|
|
63430
|
+
registry: registry,
|
|
63431
|
+
max: max,
|
|
63432
|
+
fallback: fallback,
|
|
63433
|
+
children: children
|
|
62772
63434
|
})
|
|
62773
63435
|
})]
|
|
63436
|
+
})
|
|
63437
|
+
);
|
|
63438
|
+
};
|
|
63439
|
+
const BadgeListMaxLines = ({
|
|
63440
|
+
fallback,
|
|
63441
|
+
children,
|
|
63442
|
+
max,
|
|
63443
|
+
maxLines,
|
|
63444
|
+
...boxProps
|
|
63445
|
+
}) => {
|
|
63446
|
+
const registry = useBadgeRegistry(children, true);
|
|
63447
|
+
const visibleRef = useRef();
|
|
63448
|
+
|
|
63449
|
+
// What the measure found: how many badges there were and how many of them fit
|
|
63450
|
+
// in maxLines rows. null means "not measured yet": the list then renders them
|
|
63451
|
+
// all, hidden, for the layout effect to read.
|
|
63452
|
+
const [fit, setFit] = useState(null);
|
|
63453
|
+
// The two widths this list gives whatever is around it: the one it takes with
|
|
63454
|
+
// every badge rendered, and the one it settles on once the surplus is gone.
|
|
63455
|
+
// Neither is a reason to measure again — see the resize watch below.
|
|
63456
|
+
const selfWidthsRef = useRef({
|
|
63457
|
+
full: -1,
|
|
63458
|
+
settled: -1
|
|
63459
|
+
});
|
|
63460
|
+
const measuring = fit === null;
|
|
63461
|
+
// A single badge is a single row whatever the width, so nothing can move it
|
|
63462
|
+
// out of the cap and nothing has to be watched.
|
|
63463
|
+
const watchesResize = fit !== null && fit.count > 1;
|
|
63464
|
+
|
|
63465
|
+
// Runs after every render, which is when the badges have registered and the
|
|
63466
|
+
// DOM holds whatever this render asked for.
|
|
63467
|
+
useLayoutEffect(() => {
|
|
63468
|
+
const visibleEl = visibleRef.current;
|
|
63469
|
+
if (!visibleEl) {
|
|
63470
|
+
return;
|
|
63471
|
+
}
|
|
63472
|
+
const count = registry.getEntries().length;
|
|
63473
|
+
const width = visibleEl.parentElement?.getBoundingClientRect().width ?? -1;
|
|
63474
|
+
if (fit === null) {
|
|
63475
|
+
selfWidthsRef.current.full = width;
|
|
63476
|
+
setFit({
|
|
63477
|
+
count,
|
|
63478
|
+
shown: count < 2 ? count : measureRowFit(visibleEl, maxLines)
|
|
63479
|
+
});
|
|
63480
|
+
return;
|
|
63481
|
+
}
|
|
63482
|
+
selfWidthsRef.current.settled = width;
|
|
63483
|
+
if (fit.count !== count) {
|
|
63484
|
+
// Badges came or went: what was measured no longer describes them.
|
|
63485
|
+
setFit(null);
|
|
63486
|
+
}
|
|
63487
|
+
});
|
|
63488
|
+
useLayoutEffect(() => {
|
|
63489
|
+
const outerParent = visibleRef.current?.parentElement;
|
|
63490
|
+
if (!watchesResize || !outerParent) {
|
|
63491
|
+
return undefined;
|
|
63492
|
+
}
|
|
63493
|
+
let rafId;
|
|
63494
|
+
const remeasure = () => {
|
|
63495
|
+
// Only a width change can move the rows — a height change is this list
|
|
63496
|
+
// growing or shrinking, never the room it was given.
|
|
63497
|
+
//
|
|
63498
|
+
// And not every width change either. Nothing guarantees an ancestor whose
|
|
63499
|
+
// width does not follow its content (a column with align-items: start
|
|
63500
|
+
// sizes every row to what is inside it), so rendering every badge widens
|
|
63501
|
+
// what is being watched and dropping the surplus narrows it right back.
|
|
63502
|
+
// Those two widths are this list talking to itself; measuring again on
|
|
63503
|
+
// them never ends. Any other width is the room around it changing.
|
|
63504
|
+
const width = outerParent.getBoundingClientRect().width;
|
|
63505
|
+
const {
|
|
63506
|
+
full,
|
|
63507
|
+
settled
|
|
63508
|
+
} = selfWidthsRef.current;
|
|
63509
|
+
if (Math.abs(width - full) < 0.5 || Math.abs(width - settled) < 0.5) {
|
|
63510
|
+
return;
|
|
63511
|
+
}
|
|
63512
|
+
cancelAnimationFrame(rafId);
|
|
63513
|
+
rafId = requestAnimationFrame(() => setFit(null));
|
|
63514
|
+
};
|
|
63515
|
+
const observer = new ResizeObserver(remeasure);
|
|
63516
|
+
observer.observe(outerParent);
|
|
63517
|
+
window.addEventListener("resize", remeasure);
|
|
63518
|
+
return () => {
|
|
63519
|
+
cancelAnimationFrame(rafId);
|
|
63520
|
+
observer.disconnect();
|
|
63521
|
+
window.removeEventListener("resize", remeasure);
|
|
63522
|
+
};
|
|
63523
|
+
}, [watchesResize, maxLines]);
|
|
63524
|
+
return jsx(Box, {
|
|
63525
|
+
baseClassName: "navi_badge_list",
|
|
63526
|
+
...BADGE_LIST_PROPS,
|
|
63527
|
+
...boxProps,
|
|
63528
|
+
ref: visibleRef,
|
|
63529
|
+
"navi-badge-list-measuring": measuring ? "" : undefined,
|
|
63530
|
+
children: jsx(BadgeListChildren, {
|
|
63531
|
+
registry: registry,
|
|
63532
|
+
max: max,
|
|
63533
|
+
fallback: fallback,
|
|
63534
|
+
measuring: measuring,
|
|
63535
|
+
rowFit: fit?.shown ?? null,
|
|
63536
|
+
children: children
|
|
63537
|
+
})
|
|
63538
|
+
});
|
|
63539
|
+
};
|
|
63540
|
+
const BadgeListChildren = ({
|
|
63541
|
+
registry,
|
|
63542
|
+
children,
|
|
63543
|
+
fallback,
|
|
63544
|
+
max,
|
|
63545
|
+
measuring,
|
|
63546
|
+
rowFit
|
|
63547
|
+
}) => {
|
|
63548
|
+
if (!registry) {
|
|
63549
|
+
// The badges are on their own: nothing here decides which of them render.
|
|
63550
|
+
return children;
|
|
63551
|
+
}
|
|
63552
|
+
return jsxs(Fragment$1, {
|
|
63553
|
+
children: [jsx(BadgeListContext.Provider, {
|
|
63554
|
+
value: registry,
|
|
63555
|
+
children: children
|
|
63556
|
+
}), jsx(BadgeListContent, {
|
|
63557
|
+
registry: registry,
|
|
63558
|
+
fallback: fallback,
|
|
63559
|
+
max: max,
|
|
63560
|
+
measuring: measuring,
|
|
63561
|
+
rowFit: rowFit
|
|
63562
|
+
})]
|
|
63563
|
+
});
|
|
63564
|
+
};
|
|
63565
|
+
const BadgeListContent = ({
|
|
63566
|
+
registry,
|
|
63567
|
+
fallback,
|
|
63568
|
+
max,
|
|
63569
|
+
measuring,
|
|
63570
|
+
rowFit
|
|
63571
|
+
}) => {
|
|
63572
|
+
const entries = registry.getEntries();
|
|
63573
|
+
const count = entries.length;
|
|
63574
|
+
if (count === 0) {
|
|
63575
|
+
return fallback;
|
|
63576
|
+
}
|
|
63577
|
+
|
|
63578
|
+
// The "+N" badge stands among the badges, so it takes one of the max slots
|
|
63579
|
+
// when there is a surplus to name. A list of exactly `max` badges has nothing
|
|
63580
|
+
// to name and keeps all of them.
|
|
63581
|
+
let shownCount = max !== undefined && count > max ? max - 1 : count;
|
|
63582
|
+
if (!measuring && rowFit !== null && rowFit !== undefined && rowFit < shownCount) {
|
|
63583
|
+
shownCount = rowFit;
|
|
63584
|
+
}
|
|
63585
|
+
// While measuring, everything above is on screen (hidden) along with the "+N"
|
|
63586
|
+
// badge, so the layout effect can see where the rows fall and how much room
|
|
63587
|
+
// that badge asks for. Its label then reads the worst case — every badge
|
|
63588
|
+
// hidden — so the room reserved is never short.
|
|
63589
|
+
const hasMore = measuring || shownCount < count;
|
|
63590
|
+
return jsxs(Fragment$1, {
|
|
63591
|
+
children: [entries.slice(0, shownCount).map((badgeProps, index) =>
|
|
63592
|
+
// Keyed by position: a badge's own key went to the registering vnode
|
|
63593
|
+
// above and doesn't reach here, and badges keep no state worth moving.
|
|
63594
|
+
jsx(BadgeUI, {
|
|
63595
|
+
...badgeProps
|
|
63596
|
+
}, index)), hasMore && jsx(BadgeUI, {
|
|
63597
|
+
className: "navi_badge_more",
|
|
63598
|
+
children: naviI18n("badge_list.more", {
|
|
63599
|
+
count: measuring ? count : count - shownCount
|
|
63600
|
+
})
|
|
62774
63601
|
})]
|
|
62775
63602
|
});
|
|
62776
63603
|
};
|
|
@@ -63001,7 +63828,6 @@ const PickerObjectUI = () => {
|
|
|
63001
63828
|
const PickerArray = props => {
|
|
63002
63829
|
const Next = useNextResolver();
|
|
63003
63830
|
return jsx(Next, {
|
|
63004
|
-
maxLines: "3",
|
|
63005
63831
|
ui: jsx(PickerArrayUI, {}),
|
|
63006
63832
|
...props,
|
|
63007
63833
|
type: "navi_js",
|
|
@@ -63031,6 +63857,57 @@ const PickerArrayUI = () => {
|
|
|
63031
63857
|
})
|
|
63032
63858
|
});
|
|
63033
63859
|
};
|
|
63860
|
+
|
|
63861
|
+
/**
|
|
63862
|
+
* One value the picker holds, drawn as a chip with a cross that takes it back
|
|
63863
|
+
* out. Sits wherever the application draws what was picked — on the picker's
|
|
63864
|
+
* façade (`ui`) or inside its popup — and both behave the same.
|
|
63865
|
+
*
|
|
63866
|
+
* The cross asks with `--navi-unselect` rather than writing a new list, and it
|
|
63867
|
+
* asks the picker — which holds what was picked, and hands it down to whatever
|
|
63868
|
+
* draws it in the popup. Nothing to name: the picker is the nearest control
|
|
63869
|
+
* around the chip. `commandFor` is for a chip that stands outside the picker it
|
|
63870
|
+
* speaks for.
|
|
63871
|
+
*
|
|
63872
|
+
* The cross is an own target (see own_target.js), so the press belongs to it
|
|
63873
|
+
* and not to the picker underneath, and it goes when the picker turns read-only
|
|
63874
|
+
* — a row being read still says what was picked, it just no longer offers to
|
|
63875
|
+
* unpick it.
|
|
63876
|
+
*
|
|
63877
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
63878
|
+
* value: any,
|
|
63879
|
+
* commandFor?: string,
|
|
63880
|
+
* children?: import("ignore:preact").ComponentChildren,
|
|
63881
|
+
* [key: string]: any,
|
|
63882
|
+
* }>}
|
|
63883
|
+
* @param {any} value The value this chip stands for — one entry of what the
|
|
63884
|
+
* picker holds, and what `--navi-unselect` carries.
|
|
63885
|
+
* @param {string} [commandFor] The id of the picker to take the value out of,
|
|
63886
|
+
* when the chip does not sit inside it.
|
|
63887
|
+
*/
|
|
63888
|
+
const PickerChip = ({
|
|
63889
|
+
value,
|
|
63890
|
+
commandFor,
|
|
63891
|
+
children,
|
|
63892
|
+
...rest
|
|
63893
|
+
}) => {
|
|
63894
|
+
return jsxs(Badge, {
|
|
63895
|
+
inline: true,
|
|
63896
|
+
flex: true,
|
|
63897
|
+
...rest,
|
|
63898
|
+
children: [children, jsx(Badge.Button, {
|
|
63899
|
+
ownTarget: true,
|
|
63900
|
+
command: "--navi-unselect",
|
|
63901
|
+
commandFor: commandFor,
|
|
63902
|
+
value: value,
|
|
63903
|
+
"aria-label": naviI18n("button.remove"),
|
|
63904
|
+
children: jsx(Icon, {
|
|
63905
|
+
lineOverflow: "allow",
|
|
63906
|
+
children: jsx(CloseSvg, {})
|
|
63907
|
+
})
|
|
63908
|
+
})]
|
|
63909
|
+
});
|
|
63910
|
+
};
|
|
63034
63911
|
const PickerColor = props => {
|
|
63035
63912
|
const Next = useNextResolver();
|
|
63036
63913
|
return jsx(Next, {
|
|
@@ -63469,6 +64346,30 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
|
|
|
63469
64346
|
&[navi-placeholder] {
|
|
63470
64347
|
color: var(--picker-placeholder-color);
|
|
63471
64348
|
}
|
|
64349
|
+
|
|
64350
|
+
/* A <BadgeList> caps its own rows: it renders the badges that fit and a
|
|
64351
|
+
"+N" badge for the rest, reading the number from MaxLinesContext right
|
|
64352
|
+
below. The picker must then not clamp on top of it — line-clamp turns
|
|
64353
|
+
the value into a -webkit-box and single-line truncation into a
|
|
64354
|
+
nowrap block, either of which takes the badge list out of the
|
|
64355
|
+
inline-flex layout it needs. maxLines writes those as inline styles on
|
|
64356
|
+
the element, hence !important. */
|
|
64357
|
+
&:has(.navi_badge_list) {
|
|
64358
|
+
display: inline-flex !important;
|
|
64359
|
+
-webkit-line-clamp: none !important;
|
|
64360
|
+
overflow: visible !important;
|
|
64361
|
+
-webkit-box-orient: horizontal !important;
|
|
64362
|
+
text-overflow: clip !important;
|
|
64363
|
+
white-space: normal !important;
|
|
64364
|
+
}
|
|
64365
|
+
|
|
64366
|
+
/* The façade is transparent to the pointer — a press on what it draws
|
|
64367
|
+
means "open the picker". An own target is the exception, the same way
|
|
64368
|
+
the clear cross is one in the slot below: it says the press is aimed at
|
|
64369
|
+
IT, so it has to be reachable at all. */
|
|
64370
|
+
[data-own-target] {
|
|
64371
|
+
pointer-events: auto;
|
|
64372
|
+
}
|
|
63472
64373
|
}
|
|
63473
64374
|
.navi_picker_right_slot {
|
|
63474
64375
|
display: inline-flex;
|
|
@@ -63757,214 +64658,284 @@ const PickerButton = props => {
|
|
|
63757
64658
|
// clearing being a modification like any other.
|
|
63758
64659
|
const interactive = !basePseudoState[":disabled"] && !basePseudoState[":read-only"] && !loading;
|
|
63759
64660
|
usePickerErrorCallout(uiStateController, error);
|
|
63760
|
-
return
|
|
63761
|
-
|
|
63762
|
-
|
|
63763
|
-
|
|
63764
|
-
|
|
63765
|
-
|
|
63766
|
-
|
|
63767
|
-
,
|
|
64661
|
+
return (
|
|
64662
|
+
/* Read-only crosses into everything the picker is made of: what it really
|
|
64663
|
+
holds is drawn by controls of their own — in the popup, and on the façade
|
|
64664
|
+
where an application puts its own affordances — and each of them refuses
|
|
64665
|
+
in its own words once told. Said from the read-only state alone, never
|
|
64666
|
+
from the busy one — an action running for a moment is not the same thing
|
|
64667
|
+
as a value nobody may change. */
|
|
64668
|
+
jsx(ReadOnlyContext.Provider, {
|
|
64669
|
+
value: readOnlyResolved,
|
|
64670
|
+
children: jsxs(Box, {
|
|
64671
|
+
as: "div",
|
|
64672
|
+
ref: ref
|
|
64673
|
+
// The flow this element really has (.navi_picker is display:inline-flex).
|
|
64674
|
+
// Left unsaid, Box reads a <div> as block and resolves alignX into a
|
|
64675
|
+
// text-align — which is a different intention entirely (that one is the
|
|
64676
|
+
// textAlign prop, placing the text INSIDE the value slot).
|
|
64677
|
+
,
|
|
63768
64678
|
|
|
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
|
-
|
|
64679
|
+
inline: true,
|
|
64680
|
+
flex: "x",
|
|
64681
|
+
baseClassName: "navi_picker",
|
|
64682
|
+
pseudoClasses: PICKER_BUTTON_PSEUDO_CLASSES,
|
|
64683
|
+
"data-variant": variant,
|
|
64684
|
+
"navi-picker": "",
|
|
64685
|
+
"navi-single-line": isSingleLine ? "" : undefined,
|
|
64686
|
+
"navi-ui-custom": ui === "default" ? undefined : "",
|
|
64687
|
+
"data-readonly-opens": readOnlyOpens ? "" : undefined,
|
|
64688
|
+
"data-popup-width-fit-content": popupWidthFitContent ? "" : undefined,
|
|
64689
|
+
...pickerRemainingProps,
|
|
64690
|
+
basePseudoState: basePseudoState,
|
|
64691
|
+
styleCSSVars: PickerStyleCSSVars,
|
|
64692
|
+
variant: undefined,
|
|
64693
|
+
rightSlotIcon: undefined,
|
|
64694
|
+
rightSlotIconSize: undefined,
|
|
64695
|
+
rightSlot: undefined,
|
|
64696
|
+
clearConfirm: undefined,
|
|
64697
|
+
clearConfirmPopupContent: undefined,
|
|
64698
|
+
openWhileReadOnly: undefined,
|
|
64699
|
+
ui: undefined,
|
|
64700
|
+
maxLines: undefined,
|
|
64701
|
+
popupWidthFitContent: undefined,
|
|
64702
|
+
error: undefined,
|
|
64703
|
+
dayLabel: undefined
|
|
64704
|
+
// This wrapper will receive keyboard event bubbling from the picker popup content
|
|
64705
|
+
// we re-dispatch on the input (to get escape to close for instance)
|
|
64706
|
+
,
|
|
63797
64707
|
|
|
63798
|
-
|
|
63799
|
-
|
|
63800
|
-
|
|
63801
|
-
|
|
64708
|
+
onKeyDown: inputProps.onKeyDown
|
|
64709
|
+
// in case request open/close are dispatched on the control root ->
|
|
64710
|
+
// redispatch them to the host
|
|
64711
|
+
,
|
|
63802
64712
|
|
|
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
|
|
64713
|
+
onnavi_request_open: inputProps.onnavi_request_open,
|
|
64714
|
+
onnavi_request_close: inputProps.onnavi_request_close
|
|
64715
|
+
// `--navi-select`/`--navi-unselect` about one entry of the list the
|
|
64716
|
+
// picker holds — a chip on the façade, a suggestion beside the field.
|
|
64717
|
+
// Answered here rather than by the control drawing that list in the
|
|
64718
|
+
// popup, even though rows are what such a control owns: a picker given
|
|
64719
|
+
// its own value builds its popup only on first open (see
|
|
64720
|
+
// popup_content_mount.js), so before that there is no such control at
|
|
64721
|
+
// all — and building the whole popup to have someone to talk to, for a
|
|
64722
|
+
// cross, is the wrong price. The picker holds the value in the first
|
|
64723
|
+
// place and hands it down whenever the popup is built.
|
|
63817
64724
|
,
|
|
63818
64725
|
|
|
63819
|
-
|
|
63820
|
-
|
|
63821
|
-
inputProps.onFocus?.(e);
|
|
63822
|
-
e.target.select();
|
|
64726
|
+
onnavi_request_select: e => {
|
|
64727
|
+
requestPickerListEntry(ref.current, inputRef.current, e, "select");
|
|
63823
64728
|
},
|
|
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));
|
|
64729
|
+
onnavi_request_unselect: e => {
|
|
64730
|
+
requestPickerListEntry(ref.current, inputRef.current, e, "unselect");
|
|
63837
64731
|
},
|
|
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
|
-
|
|
64732
|
+
children: [jsxs("span", {
|
|
64733
|
+
className: "navi_picker_box",
|
|
64734
|
+
children: [variant === "headless" ? null : jsx(LoadingOutline, {
|
|
64735
|
+
loading: loading,
|
|
64736
|
+
color: "var(--picker-loader-color)",
|
|
64737
|
+
inset: -2
|
|
64738
|
+
}), jsx(PickerInput, {
|
|
64739
|
+
tabIndex: variant === "headless" ? -1 : undefined,
|
|
64740
|
+
"aria-hidden": variant === "headless" ? "true" : undefined,
|
|
64741
|
+
...inputProps,
|
|
64742
|
+
// eslint-disable-next-line react/no-children-prop
|
|
64743
|
+
children: undefined // we will render children into the div
|
|
64744
|
+
,
|
|
64745
|
+
|
|
64746
|
+
ui: ui,
|
|
64747
|
+
onFocus: e => {
|
|
64748
|
+
inputProps.onFocus?.(e);
|
|
64749
|
+
e.target.select();
|
|
64750
|
+
},
|
|
64751
|
+
onCopy: e => {
|
|
64752
|
+
const pickerEl = ref.current;
|
|
64753
|
+
if (isWithinPickerContent(e.target, pickerEl)) {
|
|
64754
|
+
return;
|
|
64755
|
+
}
|
|
64756
|
+
const uiState = uiStateController.uiState;
|
|
64757
|
+
if (uiState === undefined) {
|
|
64758
|
+
return;
|
|
64759
|
+
}
|
|
64760
|
+
e.preventDefault();
|
|
64761
|
+
const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
|
|
64762
|
+
e.clipboardData.setData("text/plain", displayText);
|
|
64763
|
+
e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
|
|
64764
|
+
},
|
|
64765
|
+
onCut: e => {
|
|
64766
|
+
const pickerEl = ref.current;
|
|
64767
|
+
if (isWithinPickerContent(e.target, pickerEl)) {
|
|
64768
|
+
return;
|
|
64769
|
+
}
|
|
64770
|
+
const uiState = uiStateController.uiState;
|
|
64771
|
+
if (uiState === undefined) {
|
|
64772
|
+
return;
|
|
64773
|
+
}
|
|
64774
|
+
// the copy part don't need control to be interactable
|
|
64775
|
+
const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
|
|
64776
|
+
e.clipboardData.setData("text/plain", displayText);
|
|
64777
|
+
e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
|
|
64778
|
+
// the clear ui state part need control to be interactable
|
|
64779
|
+
dispatchRequestInteraction(pickerEl, {
|
|
64780
|
+
event: e,
|
|
64781
|
+
name: "cut",
|
|
64782
|
+
allowed: () => {
|
|
64783
|
+
dispatchRequestClearUIState(inputRef.current, e);
|
|
64784
|
+
}
|
|
63884
64785
|
});
|
|
64786
|
+
e.preventDefault();
|
|
64787
|
+
},
|
|
64788
|
+
onPaste: e => {
|
|
64789
|
+
const pickerEl = ref.current;
|
|
64790
|
+
if (isWithinPickerContent(e.target, pickerEl)) {
|
|
64791
|
+
// Don't intercept inside the picker popup content.
|
|
64792
|
+
return;
|
|
64793
|
+
}
|
|
64794
|
+
const naviData = e.clipboardData.getData("application/x-navi");
|
|
64795
|
+
let pasteValue;
|
|
64796
|
+
if (naviData) {
|
|
64797
|
+
try {
|
|
64798
|
+
pasteValue = JSON.parse(naviData);
|
|
64799
|
+
} catch {
|
|
64800
|
+
pasteValue = naviData;
|
|
64801
|
+
}
|
|
64802
|
+
} else {
|
|
64803
|
+
pasteValue = e.clipboardData.getData("text/plain");
|
|
64804
|
+
}
|
|
64805
|
+
dispatchRequestInteraction(pickerEl, {
|
|
64806
|
+
event: e,
|
|
64807
|
+
name: "paste",
|
|
64808
|
+
allowed: () => {
|
|
64809
|
+
dispatchRequestSetUIState(inputRef.current, pasteValue, {
|
|
64810
|
+
event: e
|
|
64811
|
+
});
|
|
64812
|
+
}
|
|
64813
|
+
});
|
|
64814
|
+
e.preventDefault();
|
|
63885
64815
|
}
|
|
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
|
-
|
|
64816
|
+
}), variant === "icon" || variant === "headless" || ui === "default" ? null : jsx(Text, {
|
|
64817
|
+
className: "navi_picker_value",
|
|
64818
|
+
"navi-placeholder": uiStateHoldsNothing(value) ? "" : undefined,
|
|
64819
|
+
maxLines: maxLines,
|
|
64820
|
+
children: jsx(PickerOwnContent, {
|
|
64821
|
+
children: jsx(PickerContext.Provider, {
|
|
64822
|
+
value: {
|
|
64823
|
+
value,
|
|
64824
|
+
placeholder,
|
|
64825
|
+
maxLines
|
|
64826
|
+
},
|
|
64827
|
+
children: jsx(MaxLinesContext.Provider, {
|
|
64828
|
+
value: maxLines,
|
|
64829
|
+
children: ui === undefined ? jsx(PickerDefaultUI, {}) : ui
|
|
64830
|
+
})
|
|
64831
|
+
})
|
|
64832
|
+
})
|
|
64833
|
+
}), variant === "headless" || ui === "default" ? null : jsx("span", {
|
|
64834
|
+
className: "navi_picker_right_slot",
|
|
64835
|
+
children: jsx(PickerOwnContent, {
|
|
64836
|
+
children: clearable && interactive && value !== undefined && value !== "" ? jsx(Button, {
|
|
64837
|
+
command: "--navi-clear",
|
|
64838
|
+
commandFor: inputProps.id
|
|
64839
|
+
// The question, asked before the clear rather than by the
|
|
64840
|
+
// action the clear sends — see the --navi-clear command.
|
|
64841
|
+
,
|
|
64842
|
+
|
|
64843
|
+
confirm: clearConfirm,
|
|
64844
|
+
confirmPopupContent: clearConfirmPopupContent,
|
|
64845
|
+
tabIndex: "-1"
|
|
64846
|
+
// No navi-focus-delegate, unlike the identical button inside an
|
|
64847
|
+
// input: handing focus back to the picker's own input is what
|
|
64848
|
+
// opens the popup, and clearing is the opposite intention.
|
|
64849
|
+
,
|
|
64850
|
+
|
|
64851
|
+
icon: true,
|
|
64852
|
+
variant: "discrete"
|
|
64853
|
+
// What is busy once the clear is sent is the picker — the value
|
|
64854
|
+
// being removed is the whole field's, and the picker already
|
|
64855
|
+
// draws the wait around all of it. Two outlines for one wait is
|
|
64856
|
+
// one too many.
|
|
64857
|
+
,
|
|
64858
|
+
|
|
64859
|
+
loadingOutline: false
|
|
64860
|
+
// preventDefault, not just tabIndex="-1": a mousedown focuses
|
|
64861
|
+
// its target before any click happens, and this button should
|
|
64862
|
+
// never hold focus at all — the field keeps it.
|
|
64863
|
+
,
|
|
64864
|
+
|
|
64865
|
+
onMouseDown: e => {
|
|
64866
|
+
e.preventDefault();
|
|
64867
|
+
},
|
|
64868
|
+
flex: true,
|
|
64869
|
+
align: "center",
|
|
64870
|
+
children: jsx(Icon, {
|
|
64871
|
+
size: rightSlotIconSize,
|
|
64872
|
+
lineOverflow: "allow",
|
|
64873
|
+
children: jsx(CloseSvg, {})
|
|
64874
|
+
})
|
|
64875
|
+
}) : rightSlot === undefined ?
|
|
64876
|
+
// lineOverflow: what sits in the slot is an affordance, not a
|
|
64877
|
+
// character — a caller asking for a bigger one wants it bigger,
|
|
64878
|
+
// not capped at the height of the line it sits on
|
|
64879
|
+
jsx(Icon, {
|
|
63942
64880
|
size: rightSlotIconSize,
|
|
63943
64881
|
lineOverflow: "allow",
|
|
63944
|
-
children: jsx(
|
|
63945
|
-
})
|
|
63946
|
-
})
|
|
63947
|
-
|
|
63948
|
-
|
|
63949
|
-
|
|
63950
|
-
|
|
63951
|
-
|
|
63952
|
-
|
|
63953
|
-
children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
|
|
63954
|
-
}) : rightSlot
|
|
64882
|
+
children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
|
|
64883
|
+
}) : rightSlot
|
|
64884
|
+
})
|
|
64885
|
+
})]
|
|
64886
|
+
}), jsx(ControlFacadeChildrenWrapper, {
|
|
64887
|
+
...facadeChildrenProps,
|
|
64888
|
+
children: jsx("div", {
|
|
64889
|
+
className: "navi_picker_content",
|
|
64890
|
+
children: children
|
|
63955
64891
|
})
|
|
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
|
-
})
|
|
64892
|
+
})]
|
|
63966
64893
|
})
|
|
63967
|
-
})
|
|
64894
|
+
})
|
|
64895
|
+
);
|
|
64896
|
+
};
|
|
64897
|
+
// What the picker draws itself — the value it shows, the furniture in its slot,
|
|
64898
|
+
// and whatever a caller puts in either — is not another control of the field
|
|
64899
|
+
// around it: none of it may take the id (nor the name) a <Field> hands down,
|
|
64900
|
+
// which is the picker's. Two controls under one id is one registry entry, and
|
|
64901
|
+
// the one that unmounts first — the clear cross the moment the field it emptied
|
|
64902
|
+
// is empty, a chip the moment its value is taken out — takes the picker's own
|
|
64903
|
+
// entry with it, leaving the picker looking for a controller that is gone.
|
|
64904
|
+
const PickerOwnContent = ({
|
|
64905
|
+
children
|
|
64906
|
+
}) => jsx(ControlIdContext.Provider, {
|
|
64907
|
+
value: undefined,
|
|
64908
|
+
children: jsx(ControlNameContext.Provider, {
|
|
64909
|
+
value: undefined,
|
|
64910
|
+
children: children
|
|
64911
|
+
})
|
|
64912
|
+
});
|
|
64913
|
+
|
|
64914
|
+
// `id` is what --navi-select/--navi-unselect carry (a list addresses its rows by
|
|
64915
|
+
// id); asked of a picker, what they carry is one entry of the list the picker
|
|
64916
|
+
// holds — the same thing a `<Picker.Chip value>` stands for.
|
|
64917
|
+
const requestPickerListEntry = (pickerEl, pickerInputEl, e, goal) => {
|
|
64918
|
+
const uiState = getUIStateFromElement(pickerInputEl);
|
|
64919
|
+
if (!Array.isArray(uiState)) {
|
|
64920
|
+
return;
|
|
64921
|
+
}
|
|
64922
|
+
const {
|
|
64923
|
+
id: entry
|
|
64924
|
+
} = e.detail;
|
|
64925
|
+
const isThere = uiState.some(item => compareTwoJsValues(item, entry));
|
|
64926
|
+
if (goal === "select" ? isThere : !isThere) {
|
|
64927
|
+
return;
|
|
64928
|
+
}
|
|
64929
|
+
const uiStateNext = goal === "select" ? [...uiState, entry] : uiState.filter(item => !compareTwoJsValues(item, entry));
|
|
64930
|
+
dispatchRequestInteraction(pickerEl, {
|
|
64931
|
+
event: e,
|
|
64932
|
+
name: goal,
|
|
64933
|
+
prevented: () => e.preventDefault(),
|
|
64934
|
+
allowed: () => {
|
|
64935
|
+
dispatchRequestSetUIState(pickerInputEl, uiStateNext, {
|
|
64936
|
+
event: e
|
|
64937
|
+
});
|
|
64938
|
+
}
|
|
63968
64939
|
});
|
|
63969
64940
|
};
|
|
63970
64941
|
const isWithinPickerContent = (el, pickerEl) => {
|
|
@@ -64109,6 +65080,7 @@ const PickerFirstResolver = props => {
|
|
|
64109
65080
|
});
|
|
64110
65081
|
};
|
|
64111
65082
|
const Picker = createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
|
|
65083
|
+
Picker.Chip = PickerChip;
|
|
64112
65084
|
Picker.UI = PickerDefaultUI;
|
|
64113
65085
|
Picker.UI.Date = PickerDateUI;
|
|
64114
65086
|
Picker.UI.Time = PickerTimeUI;
|
|
@@ -74904,6 +75876,9 @@ const MessageBox = ({
|
|
|
74904
75876
|
padding = "sm",
|
|
74905
75877
|
icon,
|
|
74906
75878
|
leftStripe,
|
|
75879
|
+
// A message is free text: an emoji is expected in it, and must not push
|
|
75880
|
+
// the first line down next to the icon and the close button.
|
|
75881
|
+
emojiAsIcon = true,
|
|
74907
75882
|
children,
|
|
74908
75883
|
onClose,
|
|
74909
75884
|
...rest
|
|
@@ -74941,26 +75916,39 @@ const MessageBox = ({
|
|
|
74941
75916
|
value: setHasTitleChild,
|
|
74942
75917
|
children: [icon && jsx(Icon, {
|
|
74943
75918
|
color: "var(--x-message-color)",
|
|
74944
|
-
height: "
|
|
75919
|
+
height: "1lh",
|
|
74945
75920
|
maxHeight: "auto",
|
|
74946
75921
|
selfAlignY: "start",
|
|
74947
75922
|
aspectRatio: "auto",
|
|
74948
75923
|
children: icon
|
|
74949
75924
|
}), jsx(Text, {
|
|
75925
|
+
emojiAsIcon: emojiAsIcon,
|
|
74950
75926
|
children: children
|
|
74951
|
-
}), onClose &&
|
|
74952
|
-
|
|
74953
|
-
|
|
74954
|
-
|
|
74955
|
-
|
|
75927
|
+
}), onClose &&
|
|
75928
|
+
// A column as tall as the first line of the message, pinned to the
|
|
75929
|
+
// top, the button centered in it: the close button stays level
|
|
75930
|
+
// with the first line however many lines the message takes (same
|
|
75931
|
+
// layout as Callout).
|
|
75932
|
+
jsx(Box, {
|
|
75933
|
+
flex: true,
|
|
74956
75934
|
alignY: "center",
|
|
74957
|
-
|
|
74958
|
-
|
|
74959
|
-
|
|
74960
|
-
|
|
74961
|
-
|
|
74962
|
-
|
|
74963
|
-
|
|
75935
|
+
height: "1lh",
|
|
75936
|
+
selfAlignY: "start",
|
|
75937
|
+
shrink: false,
|
|
75938
|
+
children: jsx(Button, {
|
|
75939
|
+
action: onClose,
|
|
75940
|
+
icon: true,
|
|
75941
|
+
border: "none",
|
|
75942
|
+
alignX: "center",
|
|
75943
|
+
alignY: "center",
|
|
75944
|
+
style: {
|
|
75945
|
+
":hover": {
|
|
75946
|
+
backgroundColor: "rgba(0, 0, 0, 0.1)"
|
|
75947
|
+
}
|
|
75948
|
+
},
|
|
75949
|
+
children: jsx(Icon, {
|
|
75950
|
+
children: jsx(CloseSvg, {})
|
|
75951
|
+
})
|
|
74964
75952
|
})
|
|
74965
75953
|
})]
|
|
74966
75954
|
})
|
|
@@ -77048,5 +78036,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
77048
78036
|
})
|
|
77049
78037
|
});
|
|
77050
78038
|
|
|
77051
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
|
|
78039
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, renderEmojiAsIcon, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
|
|
77052
78040
|
//# sourceMappingURL=jsenv_navi.js.map
|