@jsenv/navi 0.29.15 → 0.29.17
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 +1225 -535
- package/dist/jsenv_navi.js.map +66 -38
- package/docs/AI_INSTRUCTIONS.md +3 -0
- package/docs/z_index.md +111 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* using @jsenv/navi as intended.
|
|
4
4
|
*/
|
|
5
5
|
import { windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, installImportMetaCssBuild, coarsePointerSignal } from "./jsenv_navi_side_effects.js";
|
|
6
|
-
import { createContext, isValidElement, h, Fragment, toChildArray, render, cloneElement } from "preact";
|
|
6
|
+
import { createContext, isValidElement, h, Fragment, toChildArray, render, options, cloneElement } from "preact";
|
|
7
7
|
import { useContext, useLayoutEffect, useRef, useEffect, useCallback, useState, useMemo, useId, useErrorBoundary } from "preact/hooks";
|
|
8
8
|
import { jsx, jsxs, Fragment as Fragment$1 } from "preact/jsx-runtime";
|
|
9
9
|
import { computed, signal, effect, batch, useSignal } from "@preact/signals";
|
|
10
|
-
import { createPubSub, normalizeStyle, mergeOneStyle, getPositionedParent,
|
|
10
|
+
import { createPubSub, normalizeStyle, mergeOneStyle, getPositionedParent, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeTwoStyles, normalizeStyles, resolveCSSSize, measureLongestVisualLineWidth, hasCSSSizeUnit, resolveOklchLightness, contrastColor, createIterableWeakSet, getElementSignature, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, createEventGroupLogger, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, getKeyboardEventDefaultAction, chainEvent, activeElementSignal, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, findBefore, findAfter, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, dragAfterThreshold, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
11
11
|
export { contrastColor, startDragToReorder } from "@jsenv/dom";
|
|
12
12
|
import { createValidity, parseDuration, durationContainsNaN, compareTwoDurations, durationToSeconds, durationToISOString } from "@jsenv/validity";
|
|
13
13
|
export { compareTwoDurations, durationContainsNaN, durationToHours, durationToISOString, durationToMinutes, durationToNumber, durationToSeconds, durationToString, parseDuration } from "@jsenv/validity";
|
|
@@ -6490,6 +6490,10 @@ const POSITION_PROPS = {
|
|
|
6490
6490
|
fixed: applyToCssPropWhenTruthy("position", "fixed", "static"),
|
|
6491
6491
|
sticky: applyToCssPropWhenTruthy("position", "sticky", "static"),
|
|
6492
6492
|
zIndex: PASS_THROUGH,
|
|
6493
|
+
// Keeps the zIndex values used inside this box local to it — see
|
|
6494
|
+
// docs/z_index.md: a z-index that opens no stacking context competes with
|
|
6495
|
+
// the whole page, fixed bars included.
|
|
6496
|
+
isolation: PASS_THROUGH,
|
|
6493
6497
|
order: PASS_THROUGH,
|
|
6494
6498
|
left: (value) => {
|
|
6495
6499
|
return { left: value === true ? 0 : value };
|
|
@@ -7263,6 +7267,367 @@ const findControlRoot = (el) => {
|
|
|
7263
7267
|
return null;
|
|
7264
7268
|
};
|
|
7265
7269
|
|
|
7270
|
+
const dispatchRequestSetUIState = (element, value, detail) => {
|
|
7271
|
+
const controlHost = findControlHost(element) || element;
|
|
7272
|
+
return dispatchInternalCustomEvent(controlHost, "navi_set_ui_state", {
|
|
7273
|
+
...detail,
|
|
7274
|
+
value,
|
|
7275
|
+
});
|
|
7276
|
+
};
|
|
7277
|
+
const dispatchRequestClearUIState = (element, e) => {
|
|
7278
|
+
const controlHost = findControlHost(element) || element;
|
|
7279
|
+
return dispatchInternalCustomEvent(controlHost, "navi_clear_ui_state", {
|
|
7280
|
+
event: e,
|
|
7281
|
+
});
|
|
7282
|
+
};
|
|
7283
|
+
const dispatchRequestResetUIState = (element, e) => {
|
|
7284
|
+
const controlHost = findControlHost(element) || element;
|
|
7285
|
+
return dispatchInternalCustomEvent(controlHost, "navi_reset_ui_state", {
|
|
7286
|
+
event: e,
|
|
7287
|
+
});
|
|
7288
|
+
};
|
|
7289
|
+
/**
|
|
7290
|
+
* @param {Element} el
|
|
7291
|
+
* @param {{ own?: boolean }} [options] `own`: what the element holds BY ITSELF.
|
|
7292
|
+
* Only a button ever answers differently — one with no value of its own
|
|
7293
|
+
* inherits the value of the control around it, which is what makes
|
|
7294
|
+
* `--navi-send` on a form's button be about that form. Something asking what
|
|
7295
|
+
* THIS element says (a travel command reading what the travel is about) wants
|
|
7296
|
+
* the own value and would otherwise be handed the surrounding control's.
|
|
7297
|
+
*/
|
|
7298
|
+
const getUIStateFromElement = (el, { own } = {}) => {
|
|
7299
|
+
let uiState;
|
|
7300
|
+
dispatchInternalCustomEvent(el, "navi_get_ui_state", {
|
|
7301
|
+
own,
|
|
7302
|
+
respondWith: (v) => {
|
|
7303
|
+
uiState = v;
|
|
7304
|
+
},
|
|
7305
|
+
});
|
|
7306
|
+
return uiState;
|
|
7307
|
+
};
|
|
7308
|
+
|
|
7309
|
+
/**
|
|
7310
|
+
* Converts a JS value into the form expected by the browser DOM property for a
|
|
7311
|
+
* given control type/input type combination.
|
|
7312
|
+
*
|
|
7313
|
+
* For example:
|
|
7314
|
+
* - `datetime-local` inputs expect a local datetime string without timezone
|
|
7315
|
+
* - `number`/`range` inputs expect a numeric string or number
|
|
7316
|
+
* - `color` inputs require a non-empty hex string (falls back to `#000000`)
|
|
7317
|
+
* - All other inputs receive the value as-is (undefined → "")
|
|
7318
|
+
*
|
|
7319
|
+
* Returns either the converted value directly, or a converter function when the
|
|
7320
|
+
* conversion depends on the runtime value (e.g. plain inputs return `asInputValue`).
|
|
7321
|
+
*
|
|
7322
|
+
* @param {any} value - The JS value to convert.
|
|
7323
|
+
* @param {{ controlType: string, type: string }} options
|
|
7324
|
+
* @returns {any} The DOM-compatible value or a converter function.
|
|
7325
|
+
*/
|
|
7326
|
+
const asControlHostValue = (
|
|
7327
|
+
jsValue,
|
|
7328
|
+
{ controlType, type, inputMode },
|
|
7329
|
+
) => {
|
|
7330
|
+
if (controlType === "select") {
|
|
7331
|
+
// A select holds one of its options, always a string; holding nothing is
|
|
7332
|
+
// the empty option, which the element spells "".
|
|
7333
|
+
return asInputValue(jsValue);
|
|
7334
|
+
}
|
|
7335
|
+
if (controlType === "input" || controlType === "picker") {
|
|
7336
|
+
if (type === "datetime-local") {
|
|
7337
|
+
return asDatetimeLocalString(jsValue);
|
|
7338
|
+
}
|
|
7339
|
+
if (
|
|
7340
|
+
type === "number" ||
|
|
7341
|
+
type === "range" ||
|
|
7342
|
+
inputMode === "numeric" ||
|
|
7343
|
+
inputMode === "decimal"
|
|
7344
|
+
) {
|
|
7345
|
+
return asNumberString(jsValue);
|
|
7346
|
+
}
|
|
7347
|
+
if (type === "color") {
|
|
7348
|
+
return asColorString(jsValue);
|
|
7349
|
+
}
|
|
7350
|
+
return asInputValue(jsValue);
|
|
7351
|
+
}
|
|
7352
|
+
return jsValue;
|
|
7353
|
+
};
|
|
7354
|
+
// As explained in https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/datetime-local#setting_timezones
|
|
7355
|
+
// datetime-local does not support timezones
|
|
7356
|
+
const asDatetimeLocalString = (dateTimeString) => {
|
|
7357
|
+
const date = new Date(dateTimeString);
|
|
7358
|
+
if (isNaN(date.getTime())) {
|
|
7359
|
+
return dateTimeString;
|
|
7360
|
+
}
|
|
7361
|
+
const year = date.getFullYear();
|
|
7362
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
7363
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
7364
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
7365
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
7366
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
7367
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
|
7368
|
+
};
|
|
7369
|
+
const asNumberString = (jsValue) => {
|
|
7370
|
+
if (jsValue === undefined) {
|
|
7371
|
+
return "";
|
|
7372
|
+
}
|
|
7373
|
+
return jsValue;
|
|
7374
|
+
};
|
|
7375
|
+
// Browser requires a non-empty value for <input type="color">.
|
|
7376
|
+
// When our logical value is empty we give it #000000 so it doesn't choke.
|
|
7377
|
+
// The UI uses the original (possibly empty) value to show the checkerboard.
|
|
7378
|
+
const asColorString = (jsValue) => {
|
|
7379
|
+
return jsValue || "#000000";
|
|
7380
|
+
};
|
|
7381
|
+
const asInputValue = (jsValue) => {
|
|
7382
|
+
if (jsValue === undefined) {
|
|
7383
|
+
return "";
|
|
7384
|
+
}
|
|
7385
|
+
return jsValue;
|
|
7386
|
+
};
|
|
7387
|
+
|
|
7388
|
+
/**
|
|
7389
|
+
* Reads the current logical JS value from a control host DOM element.
|
|
7390
|
+
*
|
|
7391
|
+
* Handles all navi control host element types:
|
|
7392
|
+
* - `<button>` — reads via `navi_get_value` custom event, falls back to `button.value`
|
|
7393
|
+
* - `<input type="number|range">` — parses as a number, returns `undefined` when empty
|
|
7394
|
+
* - `<input type="checkbox|radio">` — returns `undefined` when unchecked, otherwise reads
|
|
7395
|
+
* via `navi_get_value` custom event (to preserve the original JS type of the value prop)
|
|
7396
|
+
* - `<input type="datetime-local">` — converts the local datetime string to an ISO 8601 string
|
|
7397
|
+
* - `<input type="navi_picker">` — delegates to the controller via `navi_get_ui_state`
|
|
7398
|
+
* - All other inputs — returns `input.value` as a string
|
|
7399
|
+
*
|
|
7400
|
+
* @param {HTMLElement} controlHost - The control host DOM element to read from.
|
|
7401
|
+
* @returns {any} The current logical value of the control.
|
|
7402
|
+
*/
|
|
7403
|
+
const readControlValue = (controlHost) => {
|
|
7404
|
+
if (
|
|
7405
|
+
controlHost.tagName === "BUTTON" ||
|
|
7406
|
+
controlHost.getAttribute("role") === "button"
|
|
7407
|
+
) {
|
|
7408
|
+
return readValueFromButton(controlHost);
|
|
7409
|
+
}
|
|
7410
|
+
if (controlHost.tagName === "INPUT") {
|
|
7411
|
+
// important: input.type = "navi_js"; followed by input.type; returns "text"
|
|
7412
|
+
// so use getAttribute
|
|
7413
|
+
const type = controlHost.getAttribute("type");
|
|
7414
|
+
|
|
7415
|
+
if (
|
|
7416
|
+
type === "number" ||
|
|
7417
|
+
type === "range" ||
|
|
7418
|
+
controlHost.inputMode === "numeric" ||
|
|
7419
|
+
controlHost.inputMode === "decimal"
|
|
7420
|
+
) {
|
|
7421
|
+
return readNumberFromInput(controlHost);
|
|
7422
|
+
}
|
|
7423
|
+
if (type === "color") {
|
|
7424
|
+
return readValueFromControlHost(controlHost);
|
|
7425
|
+
}
|
|
7426
|
+
if (type === "checkbox" || type === "radio") {
|
|
7427
|
+
return readValueFromCheckableInput(controlHost);
|
|
7428
|
+
}
|
|
7429
|
+
if (type === "datetime-local") {
|
|
7430
|
+
return readDatetimeLocalFromInput(controlHost);
|
|
7431
|
+
}
|
|
7432
|
+
if (type === "navi_js") {
|
|
7433
|
+
return getUIStateFromElement(controlHost);
|
|
7434
|
+
}
|
|
7435
|
+
return readValueFromInput(controlHost);
|
|
7436
|
+
}
|
|
7437
|
+
if (controlHost.hasAttribute("navi-control-host")) {
|
|
7438
|
+
// Non-button, non-input navi controls (e.g. Badge.Button rendered as span)
|
|
7439
|
+
return readValueFromControlHost(controlHost);
|
|
7440
|
+
}
|
|
7441
|
+
return readValueFromElement(controlHost);
|
|
7442
|
+
};
|
|
7443
|
+
const readValueFromControlHost = (controlHost) => {
|
|
7444
|
+
return readValueFromNaviCustomEvent(controlHost, controlHost.value);
|
|
7445
|
+
};
|
|
7446
|
+
const readValueFromButton = (button) => {
|
|
7447
|
+
return readValueFromControlHost(button);
|
|
7448
|
+
};
|
|
7449
|
+
const readDatetimeLocalFromInput = (input) => {
|
|
7450
|
+
const localDateTimeString = input.value;
|
|
7451
|
+
if (localDateTimeString === "") {
|
|
7452
|
+
return "";
|
|
7453
|
+
}
|
|
7454
|
+
const localDate = new Date(localDateTimeString);
|
|
7455
|
+
if (isNaN(localDate.getTime())) {
|
|
7456
|
+
return localDateTimeString;
|
|
7457
|
+
}
|
|
7458
|
+
return localDate.toISOString();
|
|
7459
|
+
};
|
|
7460
|
+
const readNumberFromInput = (input) => {
|
|
7461
|
+
const numberString = input.value;
|
|
7462
|
+
if (numberString === "") {
|
|
7463
|
+
return "";
|
|
7464
|
+
}
|
|
7465
|
+
const asNumber = Number(numberString);
|
|
7466
|
+
if (isNaN(asNumber)) {
|
|
7467
|
+
return numberString;
|
|
7468
|
+
}
|
|
7469
|
+
return asNumber;
|
|
7470
|
+
};
|
|
7471
|
+
const readValueFromCheckableInput = (input) => {
|
|
7472
|
+
const checked = input.checked;
|
|
7473
|
+
if (!checked) {
|
|
7474
|
+
return undefined;
|
|
7475
|
+
}
|
|
7476
|
+
return readValueFromControlHost(input);
|
|
7477
|
+
};
|
|
7478
|
+
const readValueFromInput = (input) => {
|
|
7479
|
+
const value = input.value;
|
|
7480
|
+
return value;
|
|
7481
|
+
};
|
|
7482
|
+
const readValueFromElement = (element) => {
|
|
7483
|
+
const value = element.value;
|
|
7484
|
+
return value;
|
|
7485
|
+
};
|
|
7486
|
+
const readValueFromNaviCustomEvent = (field, fallback) => {
|
|
7487
|
+
// prefer the value given as prop (respect original type, browser would convert to string)
|
|
7488
|
+
let responded;
|
|
7489
|
+
let value;
|
|
7490
|
+
dispatchCustomEvent(field, "navi_get_value", {
|
|
7491
|
+
respondWith: (jsValue) => {
|
|
7492
|
+
responded = true;
|
|
7493
|
+
value = jsValue;
|
|
7494
|
+
},
|
|
7495
|
+
});
|
|
7496
|
+
if (responded) {
|
|
7497
|
+
return value;
|
|
7498
|
+
}
|
|
7499
|
+
return fallback;
|
|
7500
|
+
};
|
|
7501
|
+
|
|
7502
|
+
// In-memory registry of all mounted ui state controllers keyed by their id.
|
|
7503
|
+
// Allows direct controller access without dispatching DOM events — used by external
|
|
7504
|
+
// callers (e.g. selectable_list) to call setUIState by id instead of via the DOM.
|
|
7505
|
+
const controllersById = new Map();
|
|
7506
|
+
|
|
7507
|
+
// In-memory registry for radio controllers, keyed by input name.
|
|
7508
|
+
// Allows radio sibling unchecking without querying the DOM — necessary when
|
|
7509
|
+
// items are virtualized and their DOM element may not exist at the time.
|
|
7510
|
+
// Form scoping is reproduced by comparing parentUIStateController references.
|
|
7511
|
+
const radioControllersByName = new Map();
|
|
7512
|
+
|
|
7513
|
+
// Registry for non-serializable JS values that cannot be written to DOM attributes as-is.
|
|
7514
|
+
// When a value is an object/array, we store it here and write a reference string to the DOM
|
|
7515
|
+
// instead of "[object Object]". Console-inspectable via window.__navi_js('id').
|
|
7516
|
+
// The controller id is used as key — if the controller has no id, the value is not registered.
|
|
7517
|
+
const naviJsRegistry = new Map();
|
|
7518
|
+
|
|
7519
|
+
const getUIStateControllerById = (id) => controllersById.get(id);
|
|
7520
|
+
const getRadioSiblings = (radioUIStateController) => {
|
|
7521
|
+
const siblings = radioControllersByName.get(radioUIStateController.name);
|
|
7522
|
+
return siblings;
|
|
7523
|
+
};
|
|
7524
|
+
|
|
7525
|
+
const toDomValue = (jsValue, { controlType, id, type, inputMode }) => {
|
|
7526
|
+
const domValue = asControlHostValue(jsValue, {
|
|
7527
|
+
controlType,
|
|
7528
|
+
type,
|
|
7529
|
+
inputMode,
|
|
7530
|
+
});
|
|
7531
|
+
if (isSerializableAsDomValue(domValue)) {
|
|
7532
|
+
return domValue;
|
|
7533
|
+
}
|
|
7534
|
+
naviJsRegistry.set(id, domValue);
|
|
7535
|
+
return `window.__navi_js('${id}')`;
|
|
7536
|
+
};
|
|
7537
|
+
|
|
7538
|
+
window.__navi_js = (id) => naviJsRegistry.get(id);
|
|
7539
|
+
const isSerializableAsDomValue = (value) => {
|
|
7540
|
+
if (value === null || value === undefined) {
|
|
7541
|
+
return true;
|
|
7542
|
+
}
|
|
7543
|
+
const type = typeof value;
|
|
7544
|
+
return type === "string" || type === "number" || type === "boolean";
|
|
7545
|
+
};
|
|
7546
|
+
|
|
7547
|
+
const onUIStateControllerCreated = (uiStateController) => {
|
|
7548
|
+
const { id, name, controlType } = uiStateController;
|
|
7549
|
+
if (id) {
|
|
7550
|
+
controllersById.set(id, uiStateController);
|
|
7551
|
+
}
|
|
7552
|
+
const proxyFor = uiStateController.props["navi-control-proxy-for"];
|
|
7553
|
+
if (proxyFor) {
|
|
7554
|
+
let proxySet = proxyControllersByRealInputId.get(proxyFor);
|
|
7555
|
+
if (!proxySet) {
|
|
7556
|
+
proxySet = new Set();
|
|
7557
|
+
proxyControllersByRealInputId.set(proxyFor, proxySet);
|
|
7558
|
+
}
|
|
7559
|
+
proxySet.add(uiStateController);
|
|
7560
|
+
}
|
|
7561
|
+
if (
|
|
7562
|
+
controlType === "input" &&
|
|
7563
|
+
uiStateController.props.type === "radio" &&
|
|
7564
|
+
name
|
|
7565
|
+
) {
|
|
7566
|
+
let set = radioControllersByName.get(name);
|
|
7567
|
+
if (!set) {
|
|
7568
|
+
set = new Set();
|
|
7569
|
+
radioControllersByName.set(name, set);
|
|
7570
|
+
}
|
|
7571
|
+
set.add(uiStateController);
|
|
7572
|
+
}
|
|
7573
|
+
};
|
|
7574
|
+
const onUIStateControllerDestroyed = (uiStateController) => {
|
|
7575
|
+
const { id, name, controlType } = uiStateController;
|
|
7576
|
+
if (id) {
|
|
7577
|
+
controllersById.delete(id);
|
|
7578
|
+
naviJsRegistry.delete(id);
|
|
7579
|
+
}
|
|
7580
|
+
const proxyFor = uiStateController.props["navi-control-proxy-for"];
|
|
7581
|
+
if (proxyFor) {
|
|
7582
|
+
const proxySet = proxyControllersByRealInputId.get(proxyFor);
|
|
7583
|
+
if (proxySet) {
|
|
7584
|
+
proxySet.delete(uiStateController);
|
|
7585
|
+
if (proxySet.size === 0) {
|
|
7586
|
+
proxyControllersByRealInputId.delete(proxyFor);
|
|
7587
|
+
}
|
|
7588
|
+
}
|
|
7589
|
+
}
|
|
7590
|
+
if (
|
|
7591
|
+
controlType === "input" &&
|
|
7592
|
+
uiStateController.controlHostProps.type === "radio" &&
|
|
7593
|
+
name
|
|
7594
|
+
) {
|
|
7595
|
+
const set = radioControllersByName.get(name);
|
|
7596
|
+
if (set) {
|
|
7597
|
+
set.delete(uiStateController);
|
|
7598
|
+
if (set.size === 0) {
|
|
7599
|
+
radioControllersByName.delete(name);
|
|
7600
|
+
}
|
|
7601
|
+
}
|
|
7602
|
+
}
|
|
7603
|
+
};
|
|
7604
|
+
|
|
7605
|
+
/**
|
|
7606
|
+
* Controller-based equivalent of findControlProxyTarget.
|
|
7607
|
+
* Given a proxy controller, returns the real control's controller.
|
|
7608
|
+
* Finds the target by walking the parent controller's children — no DOM queries.
|
|
7609
|
+
* Returns `null` when the controller is not a proxy or the target is not found.
|
|
7610
|
+
*/
|
|
7611
|
+
const findControlProxyTargetController = (controller) => {
|
|
7612
|
+
const proxyFor = controller.controlHostProps["navi-control-proxy-for"];
|
|
7613
|
+
if (!proxyFor) {
|
|
7614
|
+
return null;
|
|
7615
|
+
}
|
|
7616
|
+
return getUIStateControllerById(proxyFor) ?? null;
|
|
7617
|
+
};
|
|
7618
|
+
|
|
7619
|
+
// Reverse-lookup map: real-input id → the proxy controllers that reference it
|
|
7620
|
+
// via `navi-control-proxy-for`. A single control can be represented by several
|
|
7621
|
+
// proxies (an "enable"/"disable" button pair for one radio, for instance), so
|
|
7622
|
+
// each id holds a set. Maintained on create/destroy so lookup is O(1).
|
|
7623
|
+
const proxyControllersByRealInputId = new Map();
|
|
7624
|
+
const findProxyControllers = (realInputId) => {
|
|
7625
|
+
if (!realInputId) {
|
|
7626
|
+
return null;
|
|
7627
|
+
}
|
|
7628
|
+
return proxyControllersByRealInputId.get(realInputId) ?? null;
|
|
7629
|
+
};
|
|
7630
|
+
|
|
7266
7631
|
/**
|
|
7267
7632
|
* DOM utilities for the proxy control pattern.
|
|
7268
7633
|
*
|
|
@@ -7294,6 +7659,7 @@ const findControlRoot = (el) => {
|
|
|
7294
7659
|
* entirely. For now we keep the proxy pattern.
|
|
7295
7660
|
*/
|
|
7296
7661
|
|
|
7662
|
+
|
|
7297
7663
|
/**
|
|
7298
7664
|
* Given a proxy element, returns the real control it represents.
|
|
7299
7665
|
* Returns `null` when `el` is not a proxy.
|
|
@@ -7306,22 +7672,49 @@ const findControlProxyTarget = (el) => {
|
|
|
7306
7672
|
return document.getElementById(proxyFor);
|
|
7307
7673
|
};
|
|
7308
7674
|
|
|
7675
|
+
/**
|
|
7676
|
+
* Given a real control element, returns every proxy that visually represents
|
|
7677
|
+
* it — a control can have more than one (an "enable"/"disable" button pair for
|
|
7678
|
+
* one radio, for instance).
|
|
7679
|
+
*
|
|
7680
|
+
* Answered from the controller registry rather than the document: every proxy
|
|
7681
|
+
* declares itself through the `navi-control-proxy-for` prop, so the registry
|
|
7682
|
+
* knows them all, while asking the document means walking it in full for each
|
|
7683
|
+
* of the (overwhelmingly many) controls that have no proxy at all.
|
|
7684
|
+
*
|
|
7685
|
+
* Returns an empty array when no proxy exists for `el`.
|
|
7686
|
+
*/
|
|
7687
|
+
const findControlProxies = (el) => {
|
|
7688
|
+
if (!el.id) {
|
|
7689
|
+
return [];
|
|
7690
|
+
}
|
|
7691
|
+
const proxyControllerSet = findProxyControllers(el.id);
|
|
7692
|
+
if (!proxyControllerSet) {
|
|
7693
|
+
return [];
|
|
7694
|
+
}
|
|
7695
|
+
const proxyElements = [];
|
|
7696
|
+
for (const proxyController of proxyControllerSet) {
|
|
7697
|
+
const proxyElement = proxyController.ref.current;
|
|
7698
|
+
if (proxyElement) {
|
|
7699
|
+
proxyElements.push(proxyElement);
|
|
7700
|
+
}
|
|
7701
|
+
}
|
|
7702
|
+
return proxyElements;
|
|
7703
|
+
};
|
|
7704
|
+
|
|
7309
7705
|
/**
|
|
7310
7706
|
* Given a real control element, returns the proxy that visually represents it.
|
|
7311
7707
|
*
|
|
7312
|
-
* Use when you need
|
|
7313
|
-
*
|
|
7314
|
-
*
|
|
7708
|
+
* Use when you need a single visible stand-in for the real control — anchoring
|
|
7709
|
+
* a callout, for instance. Anything notifying proxies of a state change wants
|
|
7710
|
+
* `findControlProxies` instead, so a control represented by several of them
|
|
7711
|
+
* updates all of them.
|
|
7315
7712
|
*
|
|
7316
7713
|
* Returns `null` when no proxy exists for `el`.
|
|
7317
7714
|
*/
|
|
7318
7715
|
const findControlProxy = (el) => {
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
}
|
|
7322
|
-
return document.querySelector(
|
|
7323
|
-
`[navi-control-proxy-for="${CSS.escape(el.id)}"]`,
|
|
7324
|
-
);
|
|
7716
|
+
const [firstProxy = null] = findControlProxies(el);
|
|
7717
|
+
return firstProxy;
|
|
7325
7718
|
};
|
|
7326
7719
|
|
|
7327
7720
|
const addInputEffect = (
|
|
@@ -7580,56 +7973,16 @@ const listenInputStateChange = (
|
|
|
7580
7973
|
return teardown;
|
|
7581
7974
|
};
|
|
7582
7975
|
|
|
7583
|
-
const dispatchRequestSetUIState = (element, value, detail) => {
|
|
7584
|
-
const controlHost = findControlHost(element) || element;
|
|
7585
|
-
return dispatchInternalCustomEvent(controlHost, "navi_set_ui_state", {
|
|
7586
|
-
...detail,
|
|
7587
|
-
value,
|
|
7588
|
-
});
|
|
7589
|
-
};
|
|
7590
|
-
const dispatchRequestClearUIState = (element, e) => {
|
|
7591
|
-
const controlHost = findControlHost(element) || element;
|
|
7592
|
-
return dispatchInternalCustomEvent(controlHost, "navi_clear_ui_state", {
|
|
7593
|
-
event: e,
|
|
7594
|
-
});
|
|
7595
|
-
};
|
|
7596
|
-
const dispatchRequestResetUIState = (element, e) => {
|
|
7597
|
-
const controlHost = findControlHost(element) || element;
|
|
7598
|
-
return dispatchInternalCustomEvent(controlHost, "navi_reset_ui_state", {
|
|
7599
|
-
event: e,
|
|
7600
|
-
});
|
|
7601
|
-
};
|
|
7602
|
-
/**
|
|
7603
|
-
* @param {Element} el
|
|
7604
|
-
* @param {{ own?: boolean }} [options] `own`: what the element holds BY ITSELF.
|
|
7605
|
-
* Only a button ever answers differently — one with no value of its own
|
|
7606
|
-
* inherits the value of the control around it, which is what makes
|
|
7607
|
-
* `--navi-send` on a form's button be about that form. Something asking what
|
|
7608
|
-
* THIS element says (a travel command reading what the travel is about) wants
|
|
7609
|
-
* the own value and would otherwise be handed the surrounding control's.
|
|
7610
|
-
*/
|
|
7611
|
-
const getUIStateFromElement = (el, { own } = {}) => {
|
|
7612
|
-
let uiState;
|
|
7613
|
-
dispatchInternalCustomEvent(el, "navi_get_ui_state", {
|
|
7614
|
-
own,
|
|
7615
|
-
respondWith: (v) => {
|
|
7616
|
-
uiState = v;
|
|
7617
|
-
},
|
|
7618
|
-
});
|
|
7619
|
-
return uiState;
|
|
7620
|
-
};
|
|
7621
|
-
|
|
7622
7976
|
const requestPseudoStateCheck = (element, detail) => {
|
|
7623
7977
|
dispatchInternalCustomEvent(
|
|
7624
7978
|
element,
|
|
7625
7979
|
"navi_pseudo_state_request_check",
|
|
7626
7980
|
detail,
|
|
7627
7981
|
);
|
|
7628
|
-
// When a control has
|
|
7629
|
-
// radio with `navi-control-proxy-for`), re-check
|
|
7630
|
-
//
|
|
7631
|
-
const proxy
|
|
7632
|
-
if (proxy) {
|
|
7982
|
+
// When a control has visible proxies mirroring its state (e.g. selectable
|
|
7983
|
+
// radio with `navi-control-proxy-for`), re-check them too so they stay in
|
|
7984
|
+
// sync with the real control.
|
|
7985
|
+
for (const proxy of findControlProxies(element)) {
|
|
7633
7986
|
dispatchInternalCustomEvent(
|
|
7634
7987
|
proxy,
|
|
7635
7988
|
"navi_pseudo_state_request_check",
|
|
@@ -7682,8 +8035,7 @@ definePseudoClass(":hover", {
|
|
|
7682
8035
|
return () => {};
|
|
7683
8036
|
}
|
|
7684
8037
|
const recheckProxy = (e) => {
|
|
7685
|
-
const proxy
|
|
7686
|
-
if (proxy) {
|
|
8038
|
+
for (const proxy of findControlProxies(el)) {
|
|
7687
8039
|
requestPseudoStateCheck(proxy, { event: e });
|
|
7688
8040
|
}
|
|
7689
8041
|
};
|
|
@@ -7750,9 +8102,10 @@ definePseudoClass(":hover", {
|
|
|
7750
8102
|
if (el.matches(":hover")) {
|
|
7751
8103
|
return true;
|
|
7752
8104
|
}
|
|
7753
|
-
const proxy
|
|
7754
|
-
|
|
7755
|
-
|
|
8105
|
+
for (const proxy of findControlProxies(el)) {
|
|
8106
|
+
if (proxy.matches(":hover")) {
|
|
8107
|
+
return true;
|
|
8108
|
+
}
|
|
7756
8109
|
}
|
|
7757
8110
|
return false;
|
|
7758
8111
|
},
|
|
@@ -8114,23 +8467,30 @@ const isKeyboardModality = () => keyboardNavigationUsed;
|
|
|
8114
8467
|
requireFocusVisible
|
|
8115
8468
|
? isMatchingFocusVisible(target)
|
|
8116
8469
|
: target.matches(":focus");
|
|
8470
|
+
// Both branches of isFocusedTarget rest on :focus / :focus-visible, and only
|
|
8471
|
+
// one element in the document can match those: document.activeElement. So
|
|
8472
|
+
// the single controller worth testing is known upfront — asking the document
|
|
8473
|
+
// for every [aria-controls] would collect candidates that cannot qualify,
|
|
8474
|
+
// once per element and again on every re-check, on a document each new
|
|
8475
|
+
// element makes bigger.
|
|
8117
8476
|
const isControlledBy = (target) => {
|
|
8118
8477
|
const id = target.id;
|
|
8119
8478
|
if (!id) {
|
|
8120
8479
|
return false;
|
|
8121
8480
|
}
|
|
8122
|
-
const
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
// native (:focus-within) — no need to inherit it.
|
|
8126
|
-
if (target.contains(controller)) {
|
|
8127
|
-
continue;
|
|
8128
|
-
}
|
|
8129
|
-
if (isFocusedTarget(controller)) {
|
|
8130
|
-
return true;
|
|
8131
|
-
}
|
|
8481
|
+
const activeElement = document.activeElement;
|
|
8482
|
+
if (!activeElement || activeElement === document.body) {
|
|
8483
|
+
return false;
|
|
8132
8484
|
}
|
|
8133
|
-
|
|
8485
|
+
if (!activeElement.matches(`[aria-controls~="${id}"]`)) {
|
|
8486
|
+
return false;
|
|
8487
|
+
}
|
|
8488
|
+
// A controller inside the element it controls means focus is already
|
|
8489
|
+
// native (:focus-within) — nothing to inherit.
|
|
8490
|
+
if (target.contains(activeElement)) {
|
|
8491
|
+
return false;
|
|
8492
|
+
}
|
|
8493
|
+
return isFocusedTarget(activeElement);
|
|
8134
8494
|
};
|
|
8135
8495
|
if (isControlledBy(el)) {
|
|
8136
8496
|
return true;
|
|
@@ -8457,8 +8817,7 @@ const initPseudoStyles = (
|
|
|
8457
8817
|
}
|
|
8458
8818
|
// When this element's state changes, notify any proxy element that mirrors it
|
|
8459
8819
|
// so it can re-check and visually reflect the new state.
|
|
8460
|
-
const proxy
|
|
8461
|
-
if (proxy) {
|
|
8820
|
+
for (const proxy of findControlProxies(element)) {
|
|
8462
8821
|
requestPseudoStateCheck(proxy, {});
|
|
8463
8822
|
}
|
|
8464
8823
|
};
|
|
@@ -8905,6 +9264,20 @@ import.meta.css = [/* css */`
|
|
|
8905
9264
|
between them — and a control flush against the edge of a scrolling area
|
|
8906
9265
|
overflows it (a focus outline is drawn outside the control it belongs to)
|
|
8907
9266
|
and raises a scrollbar of its own. */
|
|
9267
|
+
/* A control sitting right against the edge of what scrolls must keep its
|
|
9268
|
+
loading outline within its own box: the outline is drawn a couple pixels
|
|
9269
|
+
outside the control (see loading_outline.jsx), and that bleed alone is
|
|
9270
|
+
enough to make the area scrollable — a scrollbar appearing and disappearing
|
|
9271
|
+
as things load. Only what the scroller directly contains is against that
|
|
9272
|
+
edge; anything nested deeper has room around it and keeps the outline it
|
|
9273
|
+
asked for, hence the child combinators. Written on the outline itself
|
|
9274
|
+
rather than on the control, because the var inherits: setting it on a
|
|
9275
|
+
container would reach every control below it, edge or not. */
|
|
9276
|
+
[data-scrollable] > .navi_loading_outline_wrapper,
|
|
9277
|
+
[data-scrollable] > * > .navi_loading_outline_wrapper {
|
|
9278
|
+
--loading-outline-min-inset: 0px;
|
|
9279
|
+
}
|
|
9280
|
+
|
|
8908
9281
|
[data-scrollable] {
|
|
8909
9282
|
overflow: var(--x-scrollable-overflow, auto);
|
|
8910
9283
|
|
|
@@ -9742,7 +10115,7 @@ const setupNetworkMonitoring = () => {
|
|
|
9742
10115
|
};
|
|
9743
10116
|
setupNetworkMonitoring();
|
|
9744
10117
|
|
|
9745
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
10118
|
+
installImportMetaCssBuild(import.meta);const css$Z = /* css */`
|
|
9746
10119
|
.navi_loading_indicator_fluid_container {
|
|
9747
10120
|
position: relative;
|
|
9748
10121
|
display: flex;
|
|
@@ -9774,7 +10147,7 @@ const LoadingIndicatorFluid = ({
|
|
|
9774
10147
|
visuallyHidden,
|
|
9775
10148
|
...rest
|
|
9776
10149
|
}) => {
|
|
9777
|
-
import.meta.css = [css$
|
|
10150
|
+
import.meta.css = [css$Z, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
|
|
9778
10151
|
const ref = useRef(null);
|
|
9779
10152
|
// The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
|
|
9780
10153
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
@@ -9979,7 +10352,7 @@ const LoadingRectangleSvg = ({
|
|
|
9979
10352
|
});
|
|
9980
10353
|
};
|
|
9981
10354
|
|
|
9982
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
10355
|
+
installImportMetaCssBuild(import.meta);const css$Y = /* css */`
|
|
9983
10356
|
.navi_loading_outline_wrapper {
|
|
9984
10357
|
position: absolute;
|
|
9985
10358
|
/* Controls place the outline slightly outside their box, right on top of
|
|
@@ -10016,7 +10389,7 @@ installImportMetaCssBuild(import.meta);const css$X = /* css */`
|
|
|
10016
10389
|
}
|
|
10017
10390
|
`;
|
|
10018
10391
|
const LoadingOutline = props => {
|
|
10019
|
-
import.meta.css = [css$
|
|
10392
|
+
import.meta.css = [css$Y, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
|
|
10020
10393
|
if (props.containerRef) {
|
|
10021
10394
|
const container = props.containerRef.current;
|
|
10022
10395
|
if (!container) {
|
|
@@ -10057,6 +10430,15 @@ const LoadingOutlineUI = props => {
|
|
|
10057
10430
|
} = props;
|
|
10058
10431
|
const shouldShowSpinner = useDebounceTrue(loading, debounce);
|
|
10059
10432
|
const rectangleRef = useRef(null);
|
|
10433
|
+
|
|
10434
|
+
// Nothing in the DOM until something actually loads: the box below is
|
|
10435
|
+
// absolutely positioned slightly outside the control, which is enough to
|
|
10436
|
+
// make an ancestor scrollable (a 1px scrollbar on a control sitting against
|
|
10437
|
+
// the edge of a scrolling area). A control that never loads must not pay for
|
|
10438
|
+
// a decoration it will never draw.
|
|
10439
|
+
if (!loading) {
|
|
10440
|
+
return children;
|
|
10441
|
+
}
|
|
10060
10442
|
let insetTop = inset + spacingTop + marginTop;
|
|
10061
10443
|
let insetRight = inset + spacingRight + marginRight;
|
|
10062
10444
|
let insetBottom = inset + spacingBottom + marginBottom;
|
|
@@ -10087,7 +10469,7 @@ const LoadingOutlineUI = props => {
|
|
|
10087
10469
|
"--loading-rectangle-bottom": `${insetBottom}px`,
|
|
10088
10470
|
"--loading-rectangle-left": `${insetLeft}px`
|
|
10089
10471
|
},
|
|
10090
|
-
children:
|
|
10472
|
+
children: jsx(LoadingIndicatorFluid, {
|
|
10091
10473
|
visuallyHidden: !shouldShowSpinner,
|
|
10092
10474
|
radius: radius,
|
|
10093
10475
|
color: color,
|
|
@@ -10341,7 +10723,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
|
|
|
10341
10723
|
};
|
|
10342
10724
|
|
|
10343
10725
|
installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
|
|
10344
|
-
const css$
|
|
10726
|
+
const css$X = /* css */`
|
|
10345
10727
|
@layer navi {
|
|
10346
10728
|
.navi_text {
|
|
10347
10729
|
&[data-skeleton] {
|
|
@@ -10847,7 +11229,7 @@ const TextShrinkWrap = props => {
|
|
|
10847
11229
|
});
|
|
10848
11230
|
};
|
|
10849
11231
|
const TextUI = props => {
|
|
10850
|
-
import.meta.css = [css$
|
|
11232
|
+
import.meta.css = [css$X, "@jsenv/navi/src/text/text.jsx"];
|
|
10851
11233
|
let {
|
|
10852
11234
|
ref,
|
|
10853
11235
|
spacing,
|
|
@@ -13164,310 +13546,6 @@ const useActionStatus = (action) => {
|
|
|
13164
13546
|
};
|
|
13165
13547
|
};
|
|
13166
13548
|
|
|
13167
|
-
/**
|
|
13168
|
-
* Converts a JS value into the form expected by the browser DOM property for a
|
|
13169
|
-
* given control type/input type combination.
|
|
13170
|
-
*
|
|
13171
|
-
* For example:
|
|
13172
|
-
* - `datetime-local` inputs expect a local datetime string without timezone
|
|
13173
|
-
* - `number`/`range` inputs expect a numeric string or number
|
|
13174
|
-
* - `color` inputs require a non-empty hex string (falls back to `#000000`)
|
|
13175
|
-
* - All other inputs receive the value as-is (undefined → "")
|
|
13176
|
-
*
|
|
13177
|
-
* Returns either the converted value directly, or a converter function when the
|
|
13178
|
-
* conversion depends on the runtime value (e.g. plain inputs return `asInputValue`).
|
|
13179
|
-
*
|
|
13180
|
-
* @param {any} value - The JS value to convert.
|
|
13181
|
-
* @param {{ controlType: string, type: string }} options
|
|
13182
|
-
* @returns {any} The DOM-compatible value or a converter function.
|
|
13183
|
-
*/
|
|
13184
|
-
const asControlHostValue = (
|
|
13185
|
-
jsValue,
|
|
13186
|
-
{ controlType, type, inputMode },
|
|
13187
|
-
) => {
|
|
13188
|
-
if (controlType === "input" || controlType === "picker") {
|
|
13189
|
-
if (type === "datetime-local") {
|
|
13190
|
-
return asDatetimeLocalString(jsValue);
|
|
13191
|
-
}
|
|
13192
|
-
if (
|
|
13193
|
-
type === "number" ||
|
|
13194
|
-
type === "range" ||
|
|
13195
|
-
inputMode === "numeric" ||
|
|
13196
|
-
inputMode === "decimal"
|
|
13197
|
-
) {
|
|
13198
|
-
return asNumberString(jsValue);
|
|
13199
|
-
}
|
|
13200
|
-
if (type === "color") {
|
|
13201
|
-
return asColorString(jsValue);
|
|
13202
|
-
}
|
|
13203
|
-
return asInputValue(jsValue);
|
|
13204
|
-
}
|
|
13205
|
-
return jsValue;
|
|
13206
|
-
};
|
|
13207
|
-
// As explained in https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/datetime-local#setting_timezones
|
|
13208
|
-
// datetime-local does not support timezones
|
|
13209
|
-
const asDatetimeLocalString = (dateTimeString) => {
|
|
13210
|
-
const date = new Date(dateTimeString);
|
|
13211
|
-
if (isNaN(date.getTime())) {
|
|
13212
|
-
return dateTimeString;
|
|
13213
|
-
}
|
|
13214
|
-
const year = date.getFullYear();
|
|
13215
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
13216
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
13217
|
-
const hours = String(date.getHours()).padStart(2, "0");
|
|
13218
|
-
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
13219
|
-
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
13220
|
-
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
|
13221
|
-
};
|
|
13222
|
-
const asNumberString = (jsValue) => {
|
|
13223
|
-
if (jsValue === undefined) {
|
|
13224
|
-
return "";
|
|
13225
|
-
}
|
|
13226
|
-
return jsValue;
|
|
13227
|
-
};
|
|
13228
|
-
// Browser requires a non-empty value for <input type="color">.
|
|
13229
|
-
// When our logical value is empty we give it #000000 so it doesn't choke.
|
|
13230
|
-
// The UI uses the original (possibly empty) value to show the checkerboard.
|
|
13231
|
-
const asColorString = (jsValue) => {
|
|
13232
|
-
return jsValue || "#000000";
|
|
13233
|
-
};
|
|
13234
|
-
const asInputValue = (jsValue) => {
|
|
13235
|
-
if (jsValue === undefined) {
|
|
13236
|
-
return "";
|
|
13237
|
-
}
|
|
13238
|
-
return jsValue;
|
|
13239
|
-
};
|
|
13240
|
-
|
|
13241
|
-
/**
|
|
13242
|
-
* Reads the current logical JS value from a control host DOM element.
|
|
13243
|
-
*
|
|
13244
|
-
* Handles all navi control host element types:
|
|
13245
|
-
* - `<button>` — reads via `navi_get_value` custom event, falls back to `button.value`
|
|
13246
|
-
* - `<input type="number|range">` — parses as a number, returns `undefined` when empty
|
|
13247
|
-
* - `<input type="checkbox|radio">` — returns `undefined` when unchecked, otherwise reads
|
|
13248
|
-
* via `navi_get_value` custom event (to preserve the original JS type of the value prop)
|
|
13249
|
-
* - `<input type="datetime-local">` — converts the local datetime string to an ISO 8601 string
|
|
13250
|
-
* - `<input type="navi_picker">` — delegates to the controller via `navi_get_ui_state`
|
|
13251
|
-
* - All other inputs — returns `input.value` as a string
|
|
13252
|
-
*
|
|
13253
|
-
* @param {HTMLElement} controlHost - The control host DOM element to read from.
|
|
13254
|
-
* @returns {any} The current logical value of the control.
|
|
13255
|
-
*/
|
|
13256
|
-
const readControlValue = (controlHost) => {
|
|
13257
|
-
if (
|
|
13258
|
-
controlHost.tagName === "BUTTON" ||
|
|
13259
|
-
controlHost.getAttribute("role") === "button"
|
|
13260
|
-
) {
|
|
13261
|
-
return readValueFromButton(controlHost);
|
|
13262
|
-
}
|
|
13263
|
-
if (controlHost.tagName === "INPUT") {
|
|
13264
|
-
// important: input.type = "navi_js"; followed by input.type; returns "text"
|
|
13265
|
-
// so use getAttribute
|
|
13266
|
-
const type = controlHost.getAttribute("type");
|
|
13267
|
-
|
|
13268
|
-
if (
|
|
13269
|
-
type === "number" ||
|
|
13270
|
-
type === "range" ||
|
|
13271
|
-
controlHost.inputMode === "numeric" ||
|
|
13272
|
-
controlHost.inputMode === "decimal"
|
|
13273
|
-
) {
|
|
13274
|
-
return readNumberFromInput(controlHost);
|
|
13275
|
-
}
|
|
13276
|
-
if (type === "color") {
|
|
13277
|
-
return readValueFromControlHost(controlHost);
|
|
13278
|
-
}
|
|
13279
|
-
if (type === "checkbox" || type === "radio") {
|
|
13280
|
-
return readValueFromCheckableInput(controlHost);
|
|
13281
|
-
}
|
|
13282
|
-
if (type === "datetime-local") {
|
|
13283
|
-
return readDatetimeLocalFromInput(controlHost);
|
|
13284
|
-
}
|
|
13285
|
-
if (type === "navi_js") {
|
|
13286
|
-
return getUIStateFromElement(controlHost);
|
|
13287
|
-
}
|
|
13288
|
-
return readValueFromInput(controlHost);
|
|
13289
|
-
}
|
|
13290
|
-
if (controlHost.hasAttribute("navi-control-host")) {
|
|
13291
|
-
// Non-button, non-input navi controls (e.g. Badge.Button rendered as span)
|
|
13292
|
-
return readValueFromControlHost(controlHost);
|
|
13293
|
-
}
|
|
13294
|
-
return readValueFromElement(controlHost);
|
|
13295
|
-
};
|
|
13296
|
-
const readValueFromControlHost = (controlHost) => {
|
|
13297
|
-
return readValueFromNaviCustomEvent(controlHost, controlHost.value);
|
|
13298
|
-
};
|
|
13299
|
-
const readValueFromButton = (button) => {
|
|
13300
|
-
return readValueFromControlHost(button);
|
|
13301
|
-
};
|
|
13302
|
-
const readDatetimeLocalFromInput = (input) => {
|
|
13303
|
-
const localDateTimeString = input.value;
|
|
13304
|
-
if (localDateTimeString === "") {
|
|
13305
|
-
return "";
|
|
13306
|
-
}
|
|
13307
|
-
const localDate = new Date(localDateTimeString);
|
|
13308
|
-
if (isNaN(localDate.getTime())) {
|
|
13309
|
-
return localDateTimeString;
|
|
13310
|
-
}
|
|
13311
|
-
return localDate.toISOString();
|
|
13312
|
-
};
|
|
13313
|
-
const readNumberFromInput = (input) => {
|
|
13314
|
-
const numberString = input.value;
|
|
13315
|
-
if (numberString === "") {
|
|
13316
|
-
return "";
|
|
13317
|
-
}
|
|
13318
|
-
const asNumber = Number(numberString);
|
|
13319
|
-
if (isNaN(asNumber)) {
|
|
13320
|
-
return numberString;
|
|
13321
|
-
}
|
|
13322
|
-
return asNumber;
|
|
13323
|
-
};
|
|
13324
|
-
const readValueFromCheckableInput = (input) => {
|
|
13325
|
-
const checked = input.checked;
|
|
13326
|
-
if (!checked) {
|
|
13327
|
-
return undefined;
|
|
13328
|
-
}
|
|
13329
|
-
return readValueFromControlHost(input);
|
|
13330
|
-
};
|
|
13331
|
-
const readValueFromInput = (input) => {
|
|
13332
|
-
const value = input.value;
|
|
13333
|
-
return value;
|
|
13334
|
-
};
|
|
13335
|
-
const readValueFromElement = (element) => {
|
|
13336
|
-
const value = element.value;
|
|
13337
|
-
return value;
|
|
13338
|
-
};
|
|
13339
|
-
const readValueFromNaviCustomEvent = (field, fallback) => {
|
|
13340
|
-
// prefer the value given as prop (respect original type, browser would convert to string)
|
|
13341
|
-
let responded;
|
|
13342
|
-
let value;
|
|
13343
|
-
dispatchCustomEvent(field, "navi_get_value", {
|
|
13344
|
-
respondWith: (jsValue) => {
|
|
13345
|
-
responded = true;
|
|
13346
|
-
value = jsValue;
|
|
13347
|
-
},
|
|
13348
|
-
});
|
|
13349
|
-
if (responded) {
|
|
13350
|
-
return value;
|
|
13351
|
-
}
|
|
13352
|
-
return fallback;
|
|
13353
|
-
};
|
|
13354
|
-
|
|
13355
|
-
// In-memory registry of all mounted ui state controllers keyed by their id.
|
|
13356
|
-
// Allows direct controller access without dispatching DOM events — used by external
|
|
13357
|
-
// callers (e.g. selectable_list) to call setUIState by id instead of via the DOM.
|
|
13358
|
-
const controllersById = new Map();
|
|
13359
|
-
|
|
13360
|
-
// In-memory registry for radio controllers, keyed by input name.
|
|
13361
|
-
// Allows radio sibling unchecking without querying the DOM — necessary when
|
|
13362
|
-
// items are virtualized and their DOM element may not exist at the time.
|
|
13363
|
-
// Form scoping is reproduced by comparing parentUIStateController references.
|
|
13364
|
-
const radioControllersByName = new Map();
|
|
13365
|
-
|
|
13366
|
-
// Registry for non-serializable JS values that cannot be written to DOM attributes as-is.
|
|
13367
|
-
// When a value is an object/array, we store it here and write a reference string to the DOM
|
|
13368
|
-
// instead of "[object Object]". Console-inspectable via window.__navi_js('id').
|
|
13369
|
-
// The controller id is used as key — if the controller has no id, the value is not registered.
|
|
13370
|
-
const naviJsRegistry = new Map();
|
|
13371
|
-
|
|
13372
|
-
const getUIStateControllerById = (id) => controllersById.get(id);
|
|
13373
|
-
const getRadioSiblings = (radioUIStateController) => {
|
|
13374
|
-
const siblings = radioControllersByName.get(radioUIStateController.name);
|
|
13375
|
-
return siblings;
|
|
13376
|
-
};
|
|
13377
|
-
|
|
13378
|
-
const toDomValue = (jsValue, { controlType, id, type, inputMode }) => {
|
|
13379
|
-
const domValue = asControlHostValue(jsValue, {
|
|
13380
|
-
controlType,
|
|
13381
|
-
type,
|
|
13382
|
-
inputMode,
|
|
13383
|
-
});
|
|
13384
|
-
if (isSerializableAsDomValue(domValue)) {
|
|
13385
|
-
return domValue;
|
|
13386
|
-
}
|
|
13387
|
-
naviJsRegistry.set(id, domValue);
|
|
13388
|
-
return `window.__navi_js('${id}')`;
|
|
13389
|
-
};
|
|
13390
|
-
|
|
13391
|
-
window.__navi_js = (id) => naviJsRegistry.get(id);
|
|
13392
|
-
const isSerializableAsDomValue = (value) => {
|
|
13393
|
-
if (value === null || value === undefined) {
|
|
13394
|
-
return true;
|
|
13395
|
-
}
|
|
13396
|
-
const type = typeof value;
|
|
13397
|
-
return type === "string" || type === "number" || type === "boolean";
|
|
13398
|
-
};
|
|
13399
|
-
|
|
13400
|
-
const onUIStateControllerCreated = (uiStateController) => {
|
|
13401
|
-
const { id, name, controlType } = uiStateController;
|
|
13402
|
-
if (id) {
|
|
13403
|
-
controllersById.set(id, uiStateController);
|
|
13404
|
-
}
|
|
13405
|
-
const proxyFor = uiStateController.props["navi-control-proxy-for"];
|
|
13406
|
-
if (proxyFor) {
|
|
13407
|
-
proxyControllerByRealInputId.set(proxyFor, uiStateController);
|
|
13408
|
-
}
|
|
13409
|
-
if (
|
|
13410
|
-
controlType === "input" &&
|
|
13411
|
-
uiStateController.props.type === "radio" &&
|
|
13412
|
-
name
|
|
13413
|
-
) {
|
|
13414
|
-
let set = radioControllersByName.get(name);
|
|
13415
|
-
if (!set) {
|
|
13416
|
-
set = new Set();
|
|
13417
|
-
radioControllersByName.set(name, set);
|
|
13418
|
-
}
|
|
13419
|
-
set.add(uiStateController);
|
|
13420
|
-
}
|
|
13421
|
-
};
|
|
13422
|
-
const onUIStateControllerDestroyed = (uiStateController) => {
|
|
13423
|
-
const { id, name, controlType } = uiStateController;
|
|
13424
|
-
if (id) {
|
|
13425
|
-
controllersById.delete(id);
|
|
13426
|
-
naviJsRegistry.delete(id);
|
|
13427
|
-
}
|
|
13428
|
-
const proxyFor = uiStateController.props["navi-control-proxy-for"];
|
|
13429
|
-
if (proxyFor) {
|
|
13430
|
-
proxyControllerByRealInputId.delete(proxyFor);
|
|
13431
|
-
}
|
|
13432
|
-
if (
|
|
13433
|
-
controlType === "input" &&
|
|
13434
|
-
uiStateController.controlHostProps.type === "radio" &&
|
|
13435
|
-
name
|
|
13436
|
-
) {
|
|
13437
|
-
const set = radioControllersByName.get(name);
|
|
13438
|
-
if (set) {
|
|
13439
|
-
set.delete(uiStateController);
|
|
13440
|
-
if (set.size === 0) {
|
|
13441
|
-
radioControllersByName.delete(name);
|
|
13442
|
-
}
|
|
13443
|
-
}
|
|
13444
|
-
}
|
|
13445
|
-
};
|
|
13446
|
-
|
|
13447
|
-
/**
|
|
13448
|
-
* Controller-based equivalent of findControlProxyTarget.
|
|
13449
|
-
* Given a proxy controller, returns the real control's controller.
|
|
13450
|
-
* Finds the target by walking the parent controller's children — no DOM queries.
|
|
13451
|
-
* Returns `null` when the controller is not a proxy or the target is not found.
|
|
13452
|
-
*/
|
|
13453
|
-
const findControlProxyTargetController = (controller) => {
|
|
13454
|
-
const proxyFor = controller.controlHostProps["navi-control-proxy-for"];
|
|
13455
|
-
if (!proxyFor) {
|
|
13456
|
-
return null;
|
|
13457
|
-
}
|
|
13458
|
-
return getUIStateControllerById(proxyFor) ?? null;
|
|
13459
|
-
};
|
|
13460
|
-
|
|
13461
|
-
// Reverse-lookup map: real-input id → proxy controller that references it via
|
|
13462
|
-
// `navi-control-proxy-for`. Maintained on create/destroy so lookup is O(1).
|
|
13463
|
-
const proxyControllerByRealInputId = new Map();
|
|
13464
|
-
const findProxyController = (realInputId) => {
|
|
13465
|
-
if (!realInputId) {
|
|
13466
|
-
return null;
|
|
13467
|
-
}
|
|
13468
|
-
return proxyControllerByRealInputId.get(realInputId) ?? null;
|
|
13469
|
-
};
|
|
13470
|
-
|
|
13471
13549
|
const CONSTRAINT_NAME_TO_PROP = {
|
|
13472
13550
|
disabled: "disabledMessage",
|
|
13473
13551
|
required: "requiredMessage",
|
|
@@ -13568,7 +13646,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
13568
13646
|
* - Arrow automatically shows when pointing at a valid anchor element
|
|
13569
13647
|
* - Centers in viewport when no anchor element provided or anchor is too big
|
|
13570
13648
|
*/
|
|
13571
|
-
const css$
|
|
13649
|
+
const css$W = /* css */`
|
|
13572
13650
|
@layer navi {
|
|
13573
13651
|
.navi_callout {
|
|
13574
13652
|
/* A callout is parented to what it explains, so it inherits from it — and
|
|
@@ -13807,7 +13885,7 @@ const openCallout = (message, {
|
|
|
13807
13885
|
skipFocus = false,
|
|
13808
13886
|
debug = () => {}
|
|
13809
13887
|
} = {}) => {
|
|
13810
|
-
import.meta.css = [css$
|
|
13888
|
+
import.meta.css = [css$W, "@jsenv/navi/src/control/rules/callout/callout.js"];
|
|
13811
13889
|
if (debug === true) {
|
|
13812
13890
|
debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
|
|
13813
13891
|
}
|
|
@@ -14408,12 +14486,12 @@ const positionCallout = (calloutElement, anchorElement, {
|
|
|
14408
14486
|
} else if (anchorElement.hasAttribute("data-callout-point-to-content-box")) {
|
|
14409
14487
|
alignToAnchorBox = "content-box";
|
|
14410
14488
|
} else {
|
|
14411
|
-
// Smart default:
|
|
14489
|
+
// Smart default: form controls and buttons are tight boxes where border-box makes sense.
|
|
14412
14490
|
// For everything else (labels, divs, fieldsets…) content-box maximizes the chance
|
|
14413
14491
|
// the arrow points at visible text rather than the outer padding/border.
|
|
14414
14492
|
const controHost = findControlHost(anchorElement) || anchorElement;
|
|
14415
14493
|
const tagName = controHost.tagName;
|
|
14416
|
-
if (tagName === "INPUT" || tagName === "BUTTON" || tagName === "FIELDSET") {
|
|
14494
|
+
if (tagName === "INPUT" || tagName === "SELECT" || tagName === "BUTTON" || tagName === "FIELDSET") {
|
|
14417
14495
|
alignToAnchorBox = "border-box";
|
|
14418
14496
|
} else {
|
|
14419
14497
|
alignToAnchorBox = "content-box";
|
|
@@ -17794,6 +17872,108 @@ const useExecuteAction = (
|
|
|
17794
17872
|
return executeAction;
|
|
17795
17873
|
};
|
|
17796
17874
|
|
|
17875
|
+
/**
|
|
17876
|
+
* A control placed inside a region that expands on click — a `<summary>`, an
|
|
17877
|
+
* accordion header carrying `aria-expanded` — has its click read twice: once by
|
|
17878
|
+
* the control it was aimed at, once by the region around it. The second reading
|
|
17879
|
+
* is never wanted; a menu opened from a collapsed row should not also unfold the
|
|
17880
|
+
* row.
|
|
17881
|
+
*
|
|
17882
|
+
* Cancelling the click is the only way to stop the region: a `<summary>` runs
|
|
17883
|
+
* its default action after the propagation, so `stopPropagation` does not reach
|
|
17884
|
+
* it. And it can only be done once the control has taken the click for itself —
|
|
17885
|
+
* navi refuses an interaction on an already-cancelled event (see
|
|
17886
|
+
* `onRequestInteraction`), so cancelling any earlier silences the control
|
|
17887
|
+
* instead of the region.
|
|
17888
|
+
*
|
|
17889
|
+
* That moment — right after an interaction was allowed — only exists inside
|
|
17890
|
+
* navi, which is why the cancellation lives here rather than in application code.
|
|
17891
|
+
*/
|
|
17892
|
+
|
|
17893
|
+
const CLICK_TO_EXPAND_SELECTOR = "summary, [aria-expanded]";
|
|
17894
|
+
|
|
17895
|
+
/**
|
|
17896
|
+
* Cancels `event` when the control consumed a click that a surrounding
|
|
17897
|
+
* click-to-expand region would otherwise read as "unfold me".
|
|
17898
|
+
*
|
|
17899
|
+
* Does nothing when cancelling the click would also cancel what the control
|
|
17900
|
+
* itself does with it (a link navigating, a checkbox toggling): there, the two
|
|
17901
|
+
* behaviours cannot be separated and the control's own comes first.
|
|
17902
|
+
*/
|
|
17903
|
+
const preventClickToExpand = (element, event) => {
|
|
17904
|
+
if (!event || event.type !== "click") {
|
|
17905
|
+
return;
|
|
17906
|
+
}
|
|
17907
|
+
if (event.defaultPrevented) {
|
|
17908
|
+
return;
|
|
17909
|
+
}
|
|
17910
|
+
if (!clickDefaultActionIsInert(element, event)) {
|
|
17911
|
+
return;
|
|
17912
|
+
}
|
|
17913
|
+
const parentElement = element.parentElement;
|
|
17914
|
+
if (!parentElement) {
|
|
17915
|
+
return;
|
|
17916
|
+
}
|
|
17917
|
+
// From the parent: a control that opens something carries its own
|
|
17918
|
+
// `aria-expanded` and would find itself.
|
|
17919
|
+
const clickToExpandRegion = parentElement.closest(CLICK_TO_EXPAND_SELECTOR);
|
|
17920
|
+
if (!clickToExpandRegion) {
|
|
17921
|
+
return;
|
|
17922
|
+
}
|
|
17923
|
+
event.preventDefault();
|
|
17924
|
+
};
|
|
17925
|
+
|
|
17926
|
+
const clickDefaultActionIsInert = (element, event) => {
|
|
17927
|
+
if (!isInertOnClick(element)) {
|
|
17928
|
+
return false;
|
|
17929
|
+
}
|
|
17930
|
+
// The activation belongs to what was clicked, which can be deeper than the
|
|
17931
|
+
// control host (a button inside it) or above it (a label wrapping it).
|
|
17932
|
+
const { target } = event;
|
|
17933
|
+
if (target && target !== element && target.nodeType === 1) {
|
|
17934
|
+
let ancestor = target;
|
|
17935
|
+
while (ancestor) {
|
|
17936
|
+
if (!isInertOnClick(ancestor)) {
|
|
17937
|
+
return false;
|
|
17938
|
+
}
|
|
17939
|
+
ancestor = ancestor.parentElement;
|
|
17940
|
+
}
|
|
17941
|
+
}
|
|
17942
|
+
return true;
|
|
17943
|
+
};
|
|
17944
|
+
|
|
17945
|
+
const NON_INERT_INPUT_TYPE_SET = new Set([
|
|
17946
|
+
"checkbox",
|
|
17947
|
+
"radio",
|
|
17948
|
+
"submit",
|
|
17949
|
+
"reset",
|
|
17950
|
+
"image",
|
|
17951
|
+
"file",
|
|
17952
|
+
]);
|
|
17953
|
+
|
|
17954
|
+
const isInertOnClick = (element) => {
|
|
17955
|
+
const { tagName } = element;
|
|
17956
|
+
if (tagName === "A" || tagName === "AREA") {
|
|
17957
|
+
return !element.hasAttribute("href");
|
|
17958
|
+
}
|
|
17959
|
+
if (tagName === "LABEL") {
|
|
17960
|
+
// A label forwards the click to its control, whose activation would be
|
|
17961
|
+
// cancelled along with the click.
|
|
17962
|
+
return false;
|
|
17963
|
+
}
|
|
17964
|
+
if (tagName === "INPUT") {
|
|
17965
|
+
return !NON_INERT_INPUT_TYPE_SET.has(element.type);
|
|
17966
|
+
}
|
|
17967
|
+
if (tagName === "BUTTON") {
|
|
17968
|
+
return element.type === "button";
|
|
17969
|
+
}
|
|
17970
|
+
if (tagName === "SELECT") {
|
|
17971
|
+
// The click opens the option list; cancelling it leaves the select shut.
|
|
17972
|
+
return false;
|
|
17973
|
+
}
|
|
17974
|
+
return true;
|
|
17975
|
+
};
|
|
17976
|
+
|
|
17797
17977
|
const BUSY_CONSTRAINT = {
|
|
17798
17978
|
name: "busy",
|
|
17799
17979
|
messageAttribute: "data-busy-message",
|
|
@@ -18162,6 +18342,9 @@ const onRequestInteraction = (
|
|
|
18162
18342
|
debugInteraction(event, `"${name}" allowed`);
|
|
18163
18343
|
allowed?.();
|
|
18164
18344
|
always?.();
|
|
18345
|
+
// The click served this control; it must not serve a second time whatever
|
|
18346
|
+
// unfolds around it (see click_to_expand.js).
|
|
18347
|
+
preventClickToExpand(controlHost, event);
|
|
18165
18348
|
return true;
|
|
18166
18349
|
};
|
|
18167
18350
|
|
|
@@ -19606,6 +19789,78 @@ createContext();
|
|
|
19606
19789
|
const ActionContext = createContext();
|
|
19607
19790
|
const ActionRequesterContext = createContext();
|
|
19608
19791
|
|
|
19792
|
+
/**
|
|
19793
|
+
* How a control tells the labels pointing at it what it is (disabled, readOnly,
|
|
19794
|
+
* required) and when it goes away.
|
|
19795
|
+
*
|
|
19796
|
+
* A label linked to its control by id has no DOM relationship to walk: the two
|
|
19797
|
+
* only know each other's id. A non-native control has no `element.labels`
|
|
19798
|
+
* either, so the only way to go from the control to its labels through the DOM
|
|
19799
|
+
* is to ask the whole document for `label[for="…"]` — once per control, on a
|
|
19800
|
+
* document that every mounted control makes bigger.
|
|
19801
|
+
*
|
|
19802
|
+
* The link is held here instead. The control publishes its state under its own
|
|
19803
|
+
* id; a label subscribes to the id it points at. Order does not matter —
|
|
19804
|
+
* whichever mounts second finds what the first left, so a label written after
|
|
19805
|
+
* its control is told just as much as one written before it.
|
|
19806
|
+
*
|
|
19807
|
+
* A label that WRAPS its control has the native relationship already
|
|
19808
|
+
* (`element.labels`) and is notified through a DOM event instead — see
|
|
19809
|
+
* `getAssociatedLabels` in control_hooks.jsx. Both channels carry the same
|
|
19810
|
+
* values and land on the same setters, so a control reachable through both is
|
|
19811
|
+
* simply told twice.
|
|
19812
|
+
*/
|
|
19813
|
+
|
|
19814
|
+
const stateByControlId = new Map();
|
|
19815
|
+
const callbackSetByControlId = new Map();
|
|
19816
|
+
|
|
19817
|
+
const publishControlStateToLabels = (controlId, controlState) => {
|
|
19818
|
+
if (!controlId) {
|
|
19819
|
+
return;
|
|
19820
|
+
}
|
|
19821
|
+
stateByControlId.set(controlId, controlState);
|
|
19822
|
+
const callbackSet = callbackSetByControlId.get(controlId);
|
|
19823
|
+
if (callbackSet) {
|
|
19824
|
+
for (const callback of callbackSet) {
|
|
19825
|
+
callback(controlState);
|
|
19826
|
+
}
|
|
19827
|
+
}
|
|
19828
|
+
};
|
|
19829
|
+
|
|
19830
|
+
const unpublishControlStateToLabels = (controlId) => {
|
|
19831
|
+
if (!controlId) {
|
|
19832
|
+
return;
|
|
19833
|
+
}
|
|
19834
|
+
stateByControlId.delete(controlId);
|
|
19835
|
+
const callbackSet = callbackSetByControlId.get(controlId);
|
|
19836
|
+
if (callbackSet) {
|
|
19837
|
+
for (const callback of callbackSet) {
|
|
19838
|
+
callback(null);
|
|
19839
|
+
}
|
|
19840
|
+
}
|
|
19841
|
+
};
|
|
19842
|
+
|
|
19843
|
+
/**
|
|
19844
|
+
* Subscribes to the state published by the control identified by `controlId`.
|
|
19845
|
+
* The callback is called right away with the current state (or `null` when no
|
|
19846
|
+
* such control is mounted), then on every change. Returns the teardown.
|
|
19847
|
+
*/
|
|
19848
|
+
const subscribeToControlState = (controlId, callback) => {
|
|
19849
|
+
let callbackSet = callbackSetByControlId.get(controlId);
|
|
19850
|
+
if (!callbackSet) {
|
|
19851
|
+
callbackSet = new Set();
|
|
19852
|
+
callbackSetByControlId.set(controlId, callbackSet);
|
|
19853
|
+
}
|
|
19854
|
+
callbackSet.add(callback);
|
|
19855
|
+
callback(stateByControlId.get(controlId) ?? null);
|
|
19856
|
+
return () => {
|
|
19857
|
+
callbackSet.delete(callback);
|
|
19858
|
+
if (callbackSet.size === 0) {
|
|
19859
|
+
callbackSetByControlId.delete(controlId);
|
|
19860
|
+
}
|
|
19861
|
+
};
|
|
19862
|
+
};
|
|
19863
|
+
|
|
19609
19864
|
/**
|
|
19610
19865
|
* Named presets for the `charGuard` prop.
|
|
19611
19866
|
* Each value is a regex character class (including the [ ] delimiters).
|
|
@@ -20209,15 +20464,17 @@ const useUIStateController = (
|
|
|
20209
20464
|
// later through a React re-render — visible as e.g. two radios
|
|
20210
20465
|
// appearing checked at once between the real input update and the
|
|
20211
20466
|
// next render (radio_sibling_uncheck case).
|
|
20212
|
-
|
|
20213
|
-
|
|
20214
|
-
|
|
20215
|
-
|
|
20216
|
-
const
|
|
20217
|
-
|
|
20218
|
-
|
|
20219
|
-
|
|
20220
|
-
|
|
20467
|
+
// Every mounted controller that declared itself as a proxy for this
|
|
20468
|
+
// one. Communicates directly to them — no DOM query needed.
|
|
20469
|
+
const proxyControllerSet = findProxyControllers(s.id);
|
|
20470
|
+
if (proxyControllerSet) {
|
|
20471
|
+
for (const proxyController of proxyControllerSet) {
|
|
20472
|
+
const mirrorEvent = new CustomEvent("proxy_mirror_state", {
|
|
20473
|
+
detail: {},
|
|
20474
|
+
});
|
|
20475
|
+
chainEvent(mirrorEvent, e);
|
|
20476
|
+
proxyController.setUIState(newUIState, mirrorEvent);
|
|
20477
|
+
}
|
|
20221
20478
|
}
|
|
20222
20479
|
}
|
|
20223
20480
|
if (isInternalEvent(e)) {
|
|
@@ -20334,8 +20591,18 @@ const useUIStateController = (
|
|
|
20334
20591
|
);
|
|
20335
20592
|
syntheticInputFired = true;
|
|
20336
20593
|
}
|
|
20594
|
+
} else if (el.tagName === "SELECT") {
|
|
20595
|
+
debugUIState(
|
|
20596
|
+
e,
|
|
20597
|
+
`dispatching synthetic input event for select "${newUIState}"`,
|
|
20598
|
+
);
|
|
20599
|
+
// A plain Event, not an InputEvent: that is what the browser
|
|
20600
|
+
// itself fires on a select, and input_effect reads the value off
|
|
20601
|
+
// the element anyway.
|
|
20602
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
20603
|
+
syntheticInputFired = true;
|
|
20337
20604
|
}
|
|
20338
|
-
// TODO:
|
|
20605
|
+
// TODO: textarea
|
|
20339
20606
|
}
|
|
20340
20607
|
}
|
|
20341
20608
|
if (!syntheticInputFired) {
|
|
@@ -21824,6 +22091,61 @@ const useControlProps = (props, {
|
|
|
21824
22091
|
}
|
|
21825
22092
|
};
|
|
21826
22093
|
}
|
|
22094
|
+
const enterToSend = e => {
|
|
22095
|
+
const control = e.currentTarget;
|
|
22096
|
+
return {
|
|
22097
|
+
name: "enter to send closest control group",
|
|
22098
|
+
bypassInteractivity: true,
|
|
22099
|
+
// allow to dispatch --navi-send even if readonly
|
|
22100
|
+
allowed: () => triggerNaviCommand(control, "--navi-send", e),
|
|
22101
|
+
// prevent dispatching click as result of this enter
|
|
22102
|
+
prevented: () => e.preventDefault()
|
|
22103
|
+
};
|
|
22104
|
+
};
|
|
22105
|
+
if (controlType === "select") {
|
|
22106
|
+
return {
|
|
22107
|
+
keyDown: e => {
|
|
22108
|
+
if (e.key === "Enter") {
|
|
22109
|
+
return enterToSend(e);
|
|
22110
|
+
}
|
|
22111
|
+
if (getKeyboardEventDefaultAction(e) === "activate") {
|
|
22112
|
+
// Space opens the list. Nothing has been chosen at that point, so
|
|
22113
|
+
// there is no ui action to trigger — only whether the list is
|
|
22114
|
+
// allowed to open at all.
|
|
22115
|
+
return {
|
|
22116
|
+
name: "keydown to open the option list",
|
|
22117
|
+
prevented: () => e.preventDefault()
|
|
22118
|
+
};
|
|
22119
|
+
}
|
|
22120
|
+
return null;
|
|
22121
|
+
},
|
|
22122
|
+
mouseDown: e => {
|
|
22123
|
+
// Same as the keydown above: opening the list is the interaction to
|
|
22124
|
+
// ask about, and refusing it is what keeps a read-only select shut.
|
|
22125
|
+
return {
|
|
22126
|
+
name: "mousedown to open the option list",
|
|
22127
|
+
prevented: () => e.preventDefault()
|
|
22128
|
+
};
|
|
22129
|
+
},
|
|
22130
|
+
input: e => {
|
|
22131
|
+
return {
|
|
22132
|
+
name: "input",
|
|
22133
|
+
allowed: () => syncUIStateWithDOM(e),
|
|
22134
|
+
// The keyboard moves the selection on a closed select, and the
|
|
22135
|
+
// platform's own list can hand back a choice, both before anything
|
|
22136
|
+
// was asked. A refused change puts the element back on the state it
|
|
22137
|
+
// never left.
|
|
22138
|
+
prevented: () => syncDomState(uiStateController.uiState, e)
|
|
22139
|
+
};
|
|
22140
|
+
},
|
|
22141
|
+
naviChange: e => {
|
|
22142
|
+
return {
|
|
22143
|
+
name: "navi_change",
|
|
22144
|
+
allowed: () => requestActionOnAllowed(e)
|
|
22145
|
+
};
|
|
22146
|
+
}
|
|
22147
|
+
};
|
|
22148
|
+
}
|
|
21827
22149
|
const keyDownDefaultOnInput = e => {
|
|
21828
22150
|
if (e.key === "Enter") {
|
|
21829
22151
|
if (actionDebounce) {
|
|
@@ -21832,15 +22154,7 @@ const useControlProps = (props, {
|
|
|
21832
22154
|
// Don't propagate to --navi-send, which would cause a double action call.
|
|
21833
22155
|
return null;
|
|
21834
22156
|
}
|
|
21835
|
-
|
|
21836
|
-
return {
|
|
21837
|
-
name: "enter on input to send closest control group",
|
|
21838
|
-
bypassInteractivity: true,
|
|
21839
|
-
// allow to dispatch --navi-send even if input is readonly
|
|
21840
|
-
allowed: () => triggerNaviCommand(input, "--navi-send", e),
|
|
21841
|
-
// prevent dispatching click as result of this enter
|
|
21842
|
-
prevented: () => e.preventDefault()
|
|
21843
|
-
};
|
|
22157
|
+
return enterToSend(e);
|
|
21844
22158
|
}
|
|
21845
22159
|
return keyDownDefault(e);
|
|
21846
22160
|
};
|
|
@@ -22236,7 +22550,7 @@ const createControlInfo = (props, {
|
|
|
22236
22550
|
defaultStatePropName = "defaultOpen";
|
|
22237
22551
|
stateInitial = props.open || props.defaultOpen;
|
|
22238
22552
|
value = props.value || "open";
|
|
22239
|
-
} else if (controlType === "picker") {
|
|
22553
|
+
} else if (controlType === "picker" || controlType === "select") {
|
|
22240
22554
|
statePropName = "value";
|
|
22241
22555
|
defaultStatePropName = "defaultValue";
|
|
22242
22556
|
if (Object.hasOwn(props, "value")) {
|
|
@@ -22255,7 +22569,10 @@ const createControlInfo = (props, {
|
|
|
22255
22569
|
stateInitial = undefined;
|
|
22256
22570
|
}
|
|
22257
22571
|
disabledSupported = true;
|
|
22258
|
-
|
|
22572
|
+
// A native <select> has no readonly attribute. What says it is read-only is
|
|
22573
|
+
// aria-readonly plus a refused interaction — see the select reactions in
|
|
22574
|
+
// getDefaultEventReactionDefinitions.
|
|
22575
|
+
readOnlySupported = controlType === "picker" && INPUT_TYPE_SUPPORTING_READONLY_SET.has(typeProp);
|
|
22259
22576
|
}
|
|
22260
22577
|
return {
|
|
22261
22578
|
controlType,
|
|
@@ -22581,39 +22898,47 @@ const useInteractiveProps = (props, {
|
|
|
22581
22898
|
controlHostProps["inert"] = "";
|
|
22582
22899
|
}
|
|
22583
22900
|
}
|
|
22584
|
-
//
|
|
22901
|
+
// Inform any associated label of our state (connected, disabled, readOnly,
|
|
22585
22902
|
// required — a Label with requiredIndicator marks itself from it rather
|
|
22586
|
-
// than being told twice what the control already knows)
|
|
22587
|
-
//
|
|
22588
|
-
// (
|
|
22903
|
+
// than being told twice what the control already knows), through both
|
|
22904
|
+
// channels a label can be reached by: a DOM event on the labels the element
|
|
22905
|
+
// itself hands over (a wrapping <label>, or a label[for] on a native form
|
|
22906
|
+
// element), and a publication under this control's id for the labels that
|
|
22907
|
+
// only know it by that (see control_label_state.js).
|
|
22908
|
+
//
|
|
22909
|
+
// The id is remembered rather than re-read at unmount time: ref.current is
|
|
22910
|
+
// often already null by then, and the labels subscribed under that id would
|
|
22911
|
+
// stay told about a control that no longer exists.
|
|
22912
|
+
const publishedIdRef = useRef(null);
|
|
22589
22913
|
useLayoutEffect(() => {
|
|
22590
22914
|
const element = ref.current;
|
|
22591
22915
|
if (!element) {
|
|
22592
22916
|
return;
|
|
22593
22917
|
}
|
|
22594
|
-
const labels = getAssociatedLabels(element);
|
|
22595
22918
|
const readOnlyForced = element.hasAttribute("data-readonly-forced");
|
|
22596
22919
|
const readOnly = readOnlyForced ? false : readOnlyResolved;
|
|
22597
|
-
|
|
22920
|
+
const controlState = {
|
|
22921
|
+
disabled: disabledResolved,
|
|
22922
|
+
readOnly,
|
|
22923
|
+
required: requiredResolved
|
|
22924
|
+
};
|
|
22925
|
+
for (const label of getAssociatedLabels(element)) {
|
|
22598
22926
|
label.dispatchEvent(new CustomEvent("navi_control_state", {
|
|
22599
|
-
detail:
|
|
22600
|
-
disabled: disabledResolved,
|
|
22601
|
-
readOnly,
|
|
22602
|
-
required: requiredResolved
|
|
22603
|
-
}
|
|
22927
|
+
detail: controlState
|
|
22604
22928
|
}));
|
|
22605
22929
|
}
|
|
22930
|
+
publishedIdRef.current = element.id;
|
|
22931
|
+
publishControlStateToLabels(element.id, controlState);
|
|
22606
22932
|
}, [disabledResolved, readOnlyResolved, requiredResolved, ref]);
|
|
22607
22933
|
useLayoutEffect(() => {
|
|
22608
22934
|
return () => {
|
|
22609
22935
|
const element = ref.current;
|
|
22610
|
-
if (
|
|
22611
|
-
|
|
22612
|
-
|
|
22613
|
-
|
|
22614
|
-
for (const label of labels) {
|
|
22615
|
-
label.dispatchEvent(new CustomEvent("navi_control_disconnected"));
|
|
22936
|
+
if (element) {
|
|
22937
|
+
for (const label of getAssociatedLabels(element)) {
|
|
22938
|
+
label.dispatchEvent(new CustomEvent("navi_control_disconnected"));
|
|
22939
|
+
}
|
|
22616
22940
|
}
|
|
22941
|
+
unpublishControlStateToLabels(publishedIdRef.current);
|
|
22617
22942
|
};
|
|
22618
22943
|
}, []);
|
|
22619
22944
|
}
|
|
@@ -22863,29 +23188,19 @@ const splitControlProps = props => {
|
|
|
22863
23188
|
}
|
|
22864
23189
|
return [controlRootProps, controlHostProps];
|
|
22865
23190
|
};
|
|
23191
|
+
|
|
23192
|
+
// The labels the DOM itself can hand over: a wrapping <label>, or a label[for]
|
|
23193
|
+
// pointing at a native form element. Everything else — a label[for] on a
|
|
23194
|
+
// non-native control — goes through control_label_state.js instead, which knows
|
|
23195
|
+
// the pairing without asking the document for it.
|
|
22866
23196
|
const getAssociatedLabels = element => {
|
|
22867
|
-
if (!element) {
|
|
23197
|
+
if (!element || !element.labels) {
|
|
22868
23198
|
return [];
|
|
22869
23199
|
}
|
|
22870
|
-
|
|
22871
|
-
// const insidePicker = closestPicker && element !== closestPicker;
|
|
22872
|
-
// const formElement = insidePicker ? closestPicker : element;
|
|
22873
|
-
const formElement = element;
|
|
22874
|
-
// Native form elements expose .labels directly
|
|
22875
|
-
if (formElement.labels && formElement.labels.length > 0) {
|
|
22876
|
-
return Array.from(formElement.labels);
|
|
22877
|
-
}
|
|
22878
|
-
const id = formElement.id;
|
|
22879
|
-
if (id) {
|
|
22880
|
-
const byId = Array.from(document.querySelectorAll(`label[for="${CSS.escape(id)}"]`));
|
|
22881
|
-
if (byId.length > 0) {
|
|
22882
|
-
return byId;
|
|
22883
|
-
}
|
|
22884
|
-
}
|
|
22885
|
-
return [];
|
|
23200
|
+
return Array.from(element.labels);
|
|
22886
23201
|
};
|
|
22887
23202
|
|
|
22888
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
23203
|
+
installImportMetaCssBuild(import.meta);const css$V = /* css */`
|
|
22889
23204
|
@layer navi {
|
|
22890
23205
|
.navi_button {
|
|
22891
23206
|
--button-border-radius: var(--navi-control-border-radius);
|
|
@@ -23278,7 +23593,7 @@ installImportMetaCssBuild(import.meta);const css$U = /* css */`
|
|
|
23278
23593
|
}
|
|
23279
23594
|
`;
|
|
23280
23595
|
const ButtonUI = props => {
|
|
23281
|
-
import.meta.css = [css$
|
|
23596
|
+
import.meta.css = [css$V, "@jsenv/navi/src/control/input/button_ui.jsx"];
|
|
23282
23597
|
const {
|
|
23283
23598
|
ref,
|
|
23284
23599
|
// href/link
|
|
@@ -24690,6 +25005,10 @@ const createOpenController = (
|
|
|
24690
25005
|
const controller = {
|
|
24691
25006
|
opened: false,
|
|
24692
25007
|
openEffect: null,
|
|
25008
|
+
// Set by the controlled element (see popup_content_mount.js) when its
|
|
25009
|
+
// content is still waiting for a first open to be built. Called below,
|
|
25010
|
+
// before openEffect, so the popup measures and positions the real thing.
|
|
25011
|
+
mountContent: null,
|
|
24693
25012
|
open: (e, detail) => {
|
|
24694
25013
|
if (controller.opened || !controller.openEffect) {
|
|
24695
25014
|
return;
|
|
@@ -24743,6 +25062,10 @@ const createOpenController = (
|
|
|
24743
25062
|
}
|
|
24744
25063
|
};
|
|
24745
25064
|
};
|
|
25065
|
+
// After prepareFocusTransfer, which has to record what held the focus
|
|
25066
|
+
// before anything inside the popup can claim it, and before openEffect,
|
|
25067
|
+
// which measures the popup to place it.
|
|
25068
|
+
controller.mountContent?.();
|
|
24746
25069
|
const openEffectReturnValue =
|
|
24747
25070
|
controller.openEffect(requestOpenEvent) || null;
|
|
24748
25071
|
openEffectCleanup = (closeEvent) => {
|
|
@@ -24936,6 +25259,84 @@ const useOpenPropsEffectOnOpenController = (openController, props) => {
|
|
|
24936
25259
|
}, [open]);
|
|
24937
25260
|
};
|
|
24938
25261
|
|
|
25262
|
+
/**
|
|
25263
|
+
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
25264
|
+
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
25265
|
+
* mounted run inside the call too, exactly as they would on any other commit.
|
|
25266
|
+
*
|
|
25267
|
+
* For the caller that has to read the DOM it just asked for — measuring an
|
|
25268
|
+
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
25269
|
+
* to do it, because what comes after is a browser event still in flight
|
|
25270
|
+
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
25271
|
+
*
|
|
25272
|
+
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
25273
|
+
* render queue drains; swapping it for "right now" for the duration of the call
|
|
25274
|
+
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
25275
|
+
* case above: rendering synchronously in the middle of an event gives up the
|
|
25276
|
+
* batching that makes several state changes one commit.
|
|
25277
|
+
*/
|
|
25278
|
+
const flushSyncRendering = (fn) => {
|
|
25279
|
+
const debounceRenderingPrevious = options.debounceRendering;
|
|
25280
|
+
options.debounceRendering = (drainRenderQueue) => {
|
|
25281
|
+
drainRenderQueue();
|
|
25282
|
+
};
|
|
25283
|
+
try {
|
|
25284
|
+
fn();
|
|
25285
|
+
} finally {
|
|
25286
|
+
options.debounceRendering = debounceRenderingPrevious;
|
|
25287
|
+
}
|
|
25288
|
+
};
|
|
25289
|
+
|
|
25290
|
+
/**
|
|
25291
|
+
* When a popup builds what it holds.
|
|
25292
|
+
*
|
|
25293
|
+
* A closed popup shows nothing, focuses nothing, and answers nothing: what it
|
|
25294
|
+
* holds is out of reach until it opens. Building that content at mount time
|
|
25295
|
+
* means a page carrying a handful of closed popups pays, on the very render
|
|
25296
|
+
* that decides how fast it appears, for content nobody has asked for — and
|
|
25297
|
+
* pays again on every subsequent measurement, since each of those nodes makes
|
|
25298
|
+
* the document the rest of the page queries bigger.
|
|
25299
|
+
*
|
|
25300
|
+
* So the content is built when the popup first opens, and stays built from
|
|
25301
|
+
* then on: closing is not throwing away, and a reopened popup finds its scroll
|
|
25302
|
+
* position, its half-typed form and its list state where it left them.
|
|
25303
|
+
*
|
|
25304
|
+
* It is built synchronously, from inside `openController.open()` and before
|
|
25305
|
+
* `openEffect` runs (see open_controller.js), so the popup still measures real
|
|
25306
|
+
* content when it positions and animates itself, and so anything inside it
|
|
25307
|
+
* still observes the opening the way it always did — mounted while the popup
|
|
25308
|
+
* reads as closed, told it opened right after (see
|
|
25309
|
+
* use_displayed_layout_effect.js).
|
|
25310
|
+
*
|
|
25311
|
+
* `mountWhenClosed` is for content something else depends on before any of
|
|
25312
|
+
* this: a value the popup's owner reads off its own children, fields a form
|
|
25313
|
+
* around it collects on submit, a size measured from outside.
|
|
25314
|
+
*/
|
|
25315
|
+
|
|
25316
|
+
|
|
25317
|
+
const usePopupContentMount = (
|
|
25318
|
+
openController,
|
|
25319
|
+
{ children, mountWhenClosed },
|
|
25320
|
+
) => {
|
|
25321
|
+
const [contentMounted, setContentMounted] = useState(
|
|
25322
|
+
() => Boolean(mountWhenClosed) || openController.opened,
|
|
25323
|
+
);
|
|
25324
|
+
openController.mountContent = contentMounted
|
|
25325
|
+
? null
|
|
25326
|
+
: () => {
|
|
25327
|
+
flushSyncRendering(() => {
|
|
25328
|
+
setContentMounted(true);
|
|
25329
|
+
});
|
|
25330
|
+
};
|
|
25331
|
+
useLayoutEffect(() => {
|
|
25332
|
+
if (mountWhenClosed) {
|
|
25333
|
+
setContentMounted(true);
|
|
25334
|
+
}
|
|
25335
|
+
}, [mountWhenClosed]);
|
|
25336
|
+
|
|
25337
|
+
return contentMounted ? children : null;
|
|
25338
|
+
};
|
|
25339
|
+
|
|
24939
25340
|
/**
|
|
24940
25341
|
* Entry/exit animation CSS shared by Popover and Dialog.
|
|
24941
25342
|
*
|
|
@@ -25406,7 +25807,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
25406
25807
|
* reaches the real container.
|
|
25407
25808
|
*/
|
|
25408
25809
|
let openLocalDialogCount = 0;
|
|
25409
|
-
const css$
|
|
25810
|
+
const css$U = /* css */`
|
|
25410
25811
|
@layer navi {
|
|
25411
25812
|
.navi_dialog {
|
|
25412
25813
|
/* Min gap between the dialog and the edges of its container. Written
|
|
@@ -25732,8 +26133,8 @@ const css$T = /* css */`
|
|
|
25732
26133
|
* the screen and a centered box ends up both cramped and out of thumb
|
|
25733
26134
|
* reach, while under a mouse the centered box is already the right shape —
|
|
25734
26135
|
* hence a prop that only ever does something on touch. It supplies defaults
|
|
25735
|
-
* for `positionArea`, `marginWithContainer` and `
|
|
25736
|
-
*
|
|
26136
|
+
* for `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`,
|
|
26137
|
+
* so any of them can still be pinned explicitly. Keyed off `(pointer: coarse)` (the
|
|
25737
26138
|
* input device, not a width breakpoint — a narrow desktop window is still a
|
|
25738
26139
|
* mouse) via `coarsePointerSignal`, so it re-resolves live.
|
|
25739
26140
|
* @param {string} [props.positionArea="center"] - Where to dock the dialog
|
|
@@ -25770,7 +26171,7 @@ const css$T = /* css */`
|
|
|
25770
26171
|
* A `layer="local"` dialog always locks its own positioned ancestor's
|
|
25771
26172
|
* scroll while open (its backdrop only covers the scrollport, so scrolling
|
|
25772
26173
|
* there would reveal uncovered content); this prop extends the lock to the
|
|
25773
|
-
* whole page.
|
|
26174
|
+
* whole page. Defaults to `true` for a dialog docked by `dockedOnTouch`.
|
|
25774
26175
|
* @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`} [props.animation]
|
|
25775
26176
|
* - `true`/`"auto"` resolves to `"scaling"` for a centered `positionArea`,
|
|
25776
26177
|
* or a concrete `"slide-from-*"` direction otherwise. Any other explicit
|
|
@@ -25813,10 +26214,15 @@ const css$T = /* css */`
|
|
|
25813
26214
|
* open controller (see `open_controller.js`) for a caller that wants to
|
|
25814
26215
|
* drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
|
|
25815
26216
|
* by `picker_custom.jsx`).
|
|
26217
|
+
* @param {boolean} [props.mountWhenClosed] - Builds `children` right away
|
|
26218
|
+
* instead of waiting for the first open (see popup_content_mount.js). For
|
|
26219
|
+
* content something depends on while the popup is still closed: a value read
|
|
26220
|
+
* off it, fields a surrounding form collects on submit, a size measured from
|
|
26221
|
+
* outside.
|
|
25816
26222
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
25817
26223
|
*/
|
|
25818
26224
|
const Dialog = props => {
|
|
25819
|
-
import.meta.css = [css$
|
|
26225
|
+
import.meta.css = [css$U, "@jsenv/navi/src/layout/dialog.jsx"];
|
|
25820
26226
|
if (props.openController) {
|
|
25821
26227
|
return jsx(ControlledDialog, {
|
|
25822
26228
|
...props
|
|
@@ -25939,7 +26345,11 @@ const DialogLocal = props => {
|
|
|
25939
26345
|
const DOCKED = {
|
|
25940
26346
|
positionArea: "bottom",
|
|
25941
26347
|
marginWithContainer: 0,
|
|
25942
|
-
expandX: true
|
|
26348
|
+
expandX: true,
|
|
26349
|
+
// A sheet resting on the bottom edge is dragged with a thumb, and a drag that
|
|
26350
|
+
// runs past its own edge must not land on the page behind it: the same
|
|
26351
|
+
// reasoning as "bottom" above, applied to the gesture instead of the shape.
|
|
26352
|
+
scrollCapture: true
|
|
25943
26353
|
};
|
|
25944
26354
|
|
|
25945
26355
|
// The first control inside `dialogEl` that is mid-action, if any. Walks the
|
|
@@ -25985,7 +26395,7 @@ const useDialogProps = props => {
|
|
|
25985
26395
|
// there's no native inert-ing, so the real backdrop below is what
|
|
25986
26396
|
// actually makes "capture"/"none" behave the same way here too.
|
|
25987
26397
|
pointerInteractionOutsideEffect = "close",
|
|
25988
|
-
scrollCapture,
|
|
26398
|
+
scrollCapture: scrollCaptureProp,
|
|
25989
26399
|
animation,
|
|
25990
26400
|
// Only ever affects --anchor-width/--anchor-height (see this file's top
|
|
25991
26401
|
// comment) — Dialog's own positioning is never relative to it.
|
|
@@ -26000,9 +26410,14 @@ const useDialogProps = props => {
|
|
|
26000
26410
|
// instead, so it's read here rather than left in `rest`.
|
|
26001
26411
|
autoFocus = "last-resort",
|
|
26002
26412
|
onKeyDown,
|
|
26003
|
-
children,
|
|
26413
|
+
children: childrenProp,
|
|
26414
|
+
mountWhenClosed,
|
|
26004
26415
|
...rest
|
|
26005
26416
|
} = props;
|
|
26417
|
+
const children = usePopupContentMount(openController, {
|
|
26418
|
+
children: childrenProp,
|
|
26419
|
+
mountWhenClosed
|
|
26420
|
+
});
|
|
26006
26421
|
const isModal = layer === "top";
|
|
26007
26422
|
const ref = props.ref;
|
|
26008
26423
|
// Only touch changes anything: with a mouse a dialog already wants to be the
|
|
@@ -26020,6 +26435,7 @@ const useDialogProps = props => {
|
|
|
26020
26435
|
const expandXUnset = expand === undefined && expandXProp === undefined;
|
|
26021
26436
|
const expandX = expandXUnset ? isDocked && DOCKED.expandX : Boolean(expand) || Boolean(expandXProp);
|
|
26022
26437
|
const expandY = Boolean(expand) || Boolean(expandYProp);
|
|
26438
|
+
const scrollCapture = scrollCaptureProp ?? (isDocked ? DOCKED.scrollCapture : false);
|
|
26023
26439
|
const backdropRef = useRef();
|
|
26024
26440
|
// Disarms a still-pending backdrop hide from a previous close (see
|
|
26025
26441
|
// armPointerDownOutsideClose below) — same pattern as popover.jsx's own.
|
|
@@ -26579,8 +26995,13 @@ const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible",
|
|
|
26579
26995
|
|
|
26580
26996
|
// Lets consumers pass animationDuration="0.5s" as a regular prop; Box maps
|
|
26581
26997
|
// it to the CSS var for us (see box.jsx's styleCSSVars handling).
|
|
26998
|
+
// borderRadius goes through --dialog-border-radius rather than the
|
|
26999
|
+
// border-radius property itself so the flush-corner rules above (a plain
|
|
27000
|
+
// stylesheet) can still square the corners that land on the container's own —
|
|
27001
|
+
// an inline border-radius would outrank them.
|
|
26582
27002
|
const DIALOG_STYLE_CSS_VARS = {
|
|
26583
27003
|
animationDuration: "--popup-animation-duration",
|
|
27004
|
+
borderRadius: "--dialog-border-radius",
|
|
26584
27005
|
minWidth: "--dialog-min-width",
|
|
26585
27006
|
maxWidth: "--dialog-max-width",
|
|
26586
27007
|
minHeight: "--dialog-min-height",
|
|
@@ -26634,7 +27055,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
26634
27055
|
* and applied.
|
|
26635
27056
|
*/
|
|
26636
27057
|
let openLocalPopoverCount = 0;
|
|
26637
|
-
const css$
|
|
27058
|
+
const css$T = /* css */`
|
|
26638
27059
|
@layer navi {
|
|
26639
27060
|
.navi_popover {
|
|
26640
27061
|
/* soft: user-configurable preferred max-height. Kept as a *default*
|
|
@@ -27001,10 +27422,15 @@ const css$S = /* css */`
|
|
|
27001
27422
|
* open controller (see `open_controller.js`) for a caller that wants to
|
|
27002
27423
|
* drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
|
|
27003
27424
|
* by `picker_custom.jsx`/`side_panel.jsx`).
|
|
27425
|
+
* @param {boolean} [props.mountWhenClosed] - Builds `children` right away
|
|
27426
|
+
* instead of waiting for the first open (see popup_content_mount.js). For
|
|
27427
|
+
* content something depends on while the popup is still closed: a value read
|
|
27428
|
+
* off it, fields a surrounding form collects on submit, a size measured from
|
|
27429
|
+
* outside.
|
|
27004
27430
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
27005
27431
|
*/
|
|
27006
27432
|
const Popover = props => {
|
|
27007
|
-
import.meta.css = [css$
|
|
27433
|
+
import.meta.css = [css$T, "@jsenv/navi/src/layout/popover.jsx"];
|
|
27008
27434
|
if (props.openController) {
|
|
27009
27435
|
return jsx(ControlledPopover, {
|
|
27010
27436
|
...props
|
|
@@ -27185,9 +27611,14 @@ const usePopoverProps = props => {
|
|
|
27185
27611
|
// instead, so it's read here rather than left in `rest`.
|
|
27186
27612
|
autoFocus = "last-resort",
|
|
27187
27613
|
onKeyDown,
|
|
27188
|
-
children,
|
|
27614
|
+
children: childrenProp,
|
|
27615
|
+
mountWhenClosed,
|
|
27189
27616
|
...rest
|
|
27190
27617
|
} = props;
|
|
27618
|
+
const children = usePopupContentMount(openController, {
|
|
27619
|
+
children: childrenProp,
|
|
27620
|
+
mountWhenClosed
|
|
27621
|
+
});
|
|
27191
27622
|
const isTopLayer = layer === "top";
|
|
27192
27623
|
const ref = props.ref;
|
|
27193
27624
|
const backdropRef = useRef();
|
|
@@ -27968,7 +28399,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
27968
28399
|
* event, and a caller replacing the body entirely then has one protocol to
|
|
27969
28400
|
* follow — `--navi-confirm` for yes, anything that closes for no.
|
|
27970
28401
|
*/
|
|
27971
|
-
const css$
|
|
28402
|
+
const css$S = /* css */`
|
|
27972
28403
|
/* The width lives on the body rather than on the popup, so that custom
|
|
27973
28404
|
content (which replaces this body entirely) sizes itself instead of
|
|
27974
28405
|
inheriting a ceiling meant for a sentence-long question. */
|
|
@@ -28105,7 +28536,7 @@ const ConfirmPopup = ({
|
|
|
28105
28536
|
onAnswer,
|
|
28106
28537
|
onClosed
|
|
28107
28538
|
}) => {
|
|
28108
|
-
import.meta.css = [css$
|
|
28539
|
+
import.meta.css = [css$S, "@jsenv/navi/src/action/confirm_popup.jsx"];
|
|
28109
28540
|
const {
|
|
28110
28541
|
mode,
|
|
28111
28542
|
confirmLabel,
|
|
@@ -28189,7 +28620,7 @@ const defaultBody = (message, {
|
|
|
28189
28620
|
});
|
|
28190
28621
|
};
|
|
28191
28622
|
|
|
28192
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
28623
|
+
installImportMetaCssBuild(import.meta);const css$R = /* css */`
|
|
28193
28624
|
.action_error {
|
|
28194
28625
|
margin-top: 0;
|
|
28195
28626
|
margin-bottom: 20px;
|
|
@@ -28214,7 +28645,7 @@ const ActionRenderer = ({
|
|
|
28214
28645
|
children,
|
|
28215
28646
|
disabled
|
|
28216
28647
|
}) => {
|
|
28217
|
-
import.meta.css = [css$
|
|
28648
|
+
import.meta.css = [css$R, "@jsenv/navi/src/action/action_renderer.jsx"];
|
|
28218
28649
|
if (action === undefined) {
|
|
28219
28650
|
throw new Error("ActionRenderer requires an action to render, but none was provided.");
|
|
28220
28651
|
}
|
|
@@ -31540,12 +31971,30 @@ const useActionAsyncData = (action, {
|
|
|
31540
31971
|
const runningState = action.runningStateSignal.peek();
|
|
31541
31972
|
const [, setTick] = useState(0);
|
|
31542
31973
|
useEffect(() => {
|
|
31543
|
-
|
|
31974
|
+
const unsubscribeFromRunningState = action.runningStateSignal.subscribe(state => {
|
|
31544
31975
|
if (state === RUNNING) {
|
|
31545
31976
|
dismissedActionWeakSet.delete(action);
|
|
31546
31977
|
}
|
|
31547
31978
|
setTick(n => n + 1);
|
|
31548
31979
|
});
|
|
31980
|
+
// The data does not come from this action's runs alone: dataSignal is a
|
|
31981
|
+
// computed over the resource store, so an other action writing that store
|
|
31982
|
+
// (a PUT upserting an item that a GET_MANY list already holds) changes the
|
|
31983
|
+
// data while this action stays COMPLETED. Subscribing here re-renders
|
|
31984
|
+
// through the same controlled path as the run state, instead of `.value`.
|
|
31985
|
+
let dataNotificationIsInitial = true;
|
|
31986
|
+
const unsubscribeFromData = action.dataSignal.subscribe(() => {
|
|
31987
|
+
if (dataNotificationIsInitial) {
|
|
31988
|
+
// subscribe() calls back synchronously with the current value
|
|
31989
|
+
dataNotificationIsInitial = false;
|
|
31990
|
+
return;
|
|
31991
|
+
}
|
|
31992
|
+
setTick(n => n + 1);
|
|
31993
|
+
});
|
|
31994
|
+
return () => {
|
|
31995
|
+
unsubscribeFromRunningState();
|
|
31996
|
+
unsubscribeFromData();
|
|
31997
|
+
};
|
|
31549
31998
|
}, []);
|
|
31550
31999
|
if (runningState === COMPLETED) {
|
|
31551
32000
|
return [action.dataSignal.peek(), false, undefined];
|
|
@@ -35391,7 +35840,7 @@ const PhoneSvg = () => {
|
|
|
35391
35840
|
};
|
|
35392
35841
|
|
|
35393
35842
|
installImportMetaCssBuild(import.meta);// # TextAnchor — how it works
|
|
35394
|
-
const css$
|
|
35843
|
+
const css$Q = /* css */`
|
|
35395
35844
|
.navi_text_anchor {
|
|
35396
35845
|
vertical-align: baseline;
|
|
35397
35846
|
user-select: none;
|
|
@@ -35426,7 +35875,7 @@ const TextAnchor = ({
|
|
|
35426
35875
|
textSize,
|
|
35427
35876
|
lineLayout
|
|
35428
35877
|
}) => {
|
|
35429
|
-
import.meta.css = [css$
|
|
35878
|
+
import.meta.css = [css$Q, "@jsenv/navi/src/text/text_anchor.jsx"];
|
|
35430
35879
|
const anchorRef = useRef();
|
|
35431
35880
|
|
|
35432
35881
|
// Plain useLayoutEffect would also fire while an ancestor dialog/popover
|
|
@@ -35541,7 +35990,7 @@ const computeTopOffset = ({
|
|
|
35541
35990
|
};
|
|
35542
35991
|
const charTopCanvas = document.createElement("canvas");
|
|
35543
35992
|
|
|
35544
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
35993
|
+
installImportMetaCssBuild(import.meta);const css$P = /* css */`
|
|
35545
35994
|
@layer navi {
|
|
35546
35995
|
/* Ensure data attributes from box.jsx can win to update display */
|
|
35547
35996
|
.navi_icon {
|
|
@@ -35699,7 +36148,7 @@ const Icon = ({
|
|
|
35699
36148
|
fillLine,
|
|
35700
36149
|
...props
|
|
35701
36150
|
}) => {
|
|
35702
|
-
import.meta.css = [css$
|
|
36151
|
+
import.meta.css = [css$P, "@jsenv/navi/src/text/icon.jsx"];
|
|
35703
36152
|
const innerChildren = href ? jsx("svg", {
|
|
35704
36153
|
width: "100%",
|
|
35705
36154
|
height: "100%",
|
|
@@ -35852,7 +36301,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
|
|
|
35852
36301
|
});
|
|
35853
36302
|
};
|
|
35854
36303
|
|
|
35855
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
36304
|
+
installImportMetaCssBuild(import.meta);const css$O = /* css */`
|
|
35856
36305
|
@layer navi {
|
|
35857
36306
|
.navi_link {
|
|
35858
36307
|
--link-border-radius: unset;
|
|
@@ -36294,7 +36743,7 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
36294
36743
|
* @param {boolean} [props.readOnly]
|
|
36295
36744
|
*/
|
|
36296
36745
|
const Link = props => {
|
|
36297
|
-
import.meta.css = [css$
|
|
36746
|
+
import.meta.css = [css$O, "@jsenv/navi/src/nav/link/link.jsx"];
|
|
36298
36747
|
if (props.route) {
|
|
36299
36748
|
return jsx(LinkWithRoute, {
|
|
36300
36749
|
...props
|
|
@@ -36529,7 +36978,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
36529
36978
|
* TabList component with support for horizontal and vertical layouts
|
|
36530
36979
|
* https://dribbble.com/search/tabs
|
|
36531
36980
|
*/
|
|
36532
|
-
const css$
|
|
36981
|
+
const css$N = /* css */`
|
|
36533
36982
|
@layer navi {
|
|
36534
36983
|
.navi_nav {
|
|
36535
36984
|
--nav-border: none;
|
|
@@ -36704,7 +37153,7 @@ const Nav = ({
|
|
|
36704
37153
|
// "before" or "after": which side the panel sits on, turning the nav into folder tabs
|
|
36705
37154
|
...props
|
|
36706
37155
|
}) => {
|
|
36707
|
-
import.meta.css = [css$
|
|
37156
|
+
import.meta.css = [css$N, "@jsenv/navi/src/nav/link/nav.jsx"];
|
|
36708
37157
|
children = toChildArray(children);
|
|
36709
37158
|
return jsx(Box, {
|
|
36710
37159
|
as: "nav",
|
|
@@ -37096,7 +37545,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
37096
37545
|
* Border width participates in layout (it is added to the tab and page
|
|
37097
37546
|
* padding): a thick border grows the binder rather than eating into the text.
|
|
37098
37547
|
*/
|
|
37099
|
-
const css$
|
|
37548
|
+
const css$M = /* css */`
|
|
37100
37549
|
@layer navi {
|
|
37101
37550
|
.navi_binder {
|
|
37102
37551
|
--binder-border-width: var(--navi-control-border-width);
|
|
@@ -37409,7 +37858,7 @@ const Binder = ({
|
|
|
37409
37858
|
pagePadding,
|
|
37410
37859
|
...props
|
|
37411
37860
|
}) => {
|
|
37412
|
-
import.meta.css = [css$
|
|
37861
|
+
import.meta.css = [css$M, "@jsenv/navi/src/nav/binder/binder.jsx"];
|
|
37413
37862
|
const items = toChildArray(children).map((child, index) => {
|
|
37414
37863
|
const {
|
|
37415
37864
|
value: itemValue,
|
|
@@ -37935,7 +38384,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
37935
38384
|
* into the size; a box-shadow draws the identical line and stays out of
|
|
37936
38385
|
* layout.
|
|
37937
38386
|
*/
|
|
37938
|
-
const css$
|
|
38387
|
+
const css$L = /* css */`
|
|
37939
38388
|
@layer navi {
|
|
37940
38389
|
:root {
|
|
37941
38390
|
--navi-fixed-bar-width: 56px;
|
|
@@ -38073,7 +38522,7 @@ const FixedBar = ({
|
|
|
38073
38522
|
border = true,
|
|
38074
38523
|
...props
|
|
38075
38524
|
}) => {
|
|
38076
|
-
import.meta.css = [css$
|
|
38525
|
+
import.meta.css = [css$L, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
|
|
38077
38526
|
const defaultRef = useRef();
|
|
38078
38527
|
props.ref = props.ref || defaultRef;
|
|
38079
38528
|
// Whichever of width/height crosses the edge the bar sits on is what the
|
|
@@ -38161,7 +38610,7 @@ const FixedBar = ({
|
|
|
38161
38610
|
// Subpixel layout rounds rectangles up on boxes that fit exactly.
|
|
38162
38611
|
const OVERFLOW_TOLERANCE = 1;
|
|
38163
38612
|
|
|
38164
|
-
const css$
|
|
38613
|
+
const css$K = /* css */ `
|
|
38165
38614
|
[data-navi-overflow-x] {
|
|
38166
38615
|
outline: 2px dashed #e74c3c;
|
|
38167
38616
|
outline-offset: -2px;
|
|
@@ -38185,7 +38634,7 @@ const detectHorizontalOverflow = ({
|
|
|
38185
38634
|
let styleEl = null;
|
|
38186
38635
|
if (highlight) {
|
|
38187
38636
|
styleEl = document.createElement("style");
|
|
38188
|
-
styleEl.textContent = css$
|
|
38637
|
+
styleEl.textContent = css$K;
|
|
38189
38638
|
document.head.appendChild(styleEl);
|
|
38190
38639
|
}
|
|
38191
38640
|
|
|
@@ -38341,7 +38790,7 @@ const useFocusGroup = (
|
|
|
38341
38790
|
|
|
38342
38791
|
installImportMetaCssBuild(import.meta);const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
|
|
38343
38792
|
const downArrowPath = "M480-280L160-600l80-80 240 240 240-240 80 80-320 320z";
|
|
38344
|
-
const css$
|
|
38793
|
+
const css$J = /* css */`
|
|
38345
38794
|
.navi_summary_marker {
|
|
38346
38795
|
width: 1em;
|
|
38347
38796
|
height: 1em;
|
|
@@ -38426,7 +38875,7 @@ const SummaryMarker = ({
|
|
|
38426
38875
|
open,
|
|
38427
38876
|
loading
|
|
38428
38877
|
}) => {
|
|
38429
|
-
import.meta.css = [css$
|
|
38878
|
+
import.meta.css = [css$J, "@jsenv/navi/src/control/details/summary_marker.jsx"];
|
|
38430
38879
|
const showLoading = useDebounceTrue(loading, 300);
|
|
38431
38880
|
const mountedRef = useRef(false);
|
|
38432
38881
|
const prevOpenRef = useRef(open);
|
|
@@ -38480,7 +38929,7 @@ const SummaryMarker = ({
|
|
|
38480
38929
|
});
|
|
38481
38930
|
};
|
|
38482
38931
|
|
|
38483
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
38932
|
+
installImportMetaCssBuild(import.meta);const css$I = /* css */`
|
|
38484
38933
|
.navi_details {
|
|
38485
38934
|
position: relative;
|
|
38486
38935
|
z-index: 1;
|
|
@@ -38526,7 +38975,7 @@ const Details = props => {
|
|
|
38526
38975
|
return details;
|
|
38527
38976
|
};
|
|
38528
38977
|
const DetailsField = props => {
|
|
38529
|
-
import.meta.css = [css$
|
|
38978
|
+
import.meta.css = [css$I, "@jsenv/navi/src/control/details/details.jsx"];
|
|
38530
38979
|
const {
|
|
38531
38980
|
ref,
|
|
38532
38981
|
persists,
|
|
@@ -38782,7 +39231,7 @@ const ControlGroup = props => {
|
|
|
38782
39231
|
};
|
|
38783
39232
|
const CONTROL_GROUP_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
|
|
38784
39233
|
|
|
38785
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
39234
|
+
installImportMetaCssBuild(import.meta);const css$H = /* css */`
|
|
38786
39235
|
@layer navi {
|
|
38787
39236
|
.navi_checkbox {
|
|
38788
39237
|
--switch-margin: 0; /* Useful to reserve space for outline */
|
|
@@ -38860,7 +39309,7 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
|
|
|
38860
39309
|
}
|
|
38861
39310
|
`;
|
|
38862
39311
|
const SwitchUI = () => {
|
|
38863
|
-
import.meta.css = [css$
|
|
39312
|
+
import.meta.css = [css$H, "@jsenv/navi/src/control/input/switch_ui.jsx"];
|
|
38864
39313
|
return jsx(Box, {
|
|
38865
39314
|
className: "navi_switch",
|
|
38866
39315
|
as: "svg",
|
|
@@ -38902,7 +39351,7 @@ const useCheckableProps = (props, options) => {
|
|
|
38902
39351
|
return result;
|
|
38903
39352
|
};
|
|
38904
39353
|
|
|
38905
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
39354
|
+
installImportMetaCssBuild(import.meta);const css$G = /* css */`
|
|
38906
39355
|
@layer navi {
|
|
38907
39356
|
.navi_checkbox {
|
|
38908
39357
|
--border-radius: var(--navi-checkbox-border-radius);
|
|
@@ -39229,7 +39678,7 @@ const InputCheckboxHeadless = props => {
|
|
|
39229
39678
|
});
|
|
39230
39679
|
};
|
|
39231
39680
|
const InputCheckboxFieldInterface = props => {
|
|
39232
|
-
import.meta.css = [css$
|
|
39681
|
+
import.meta.css = [css$G, "@jsenv/navi/src/control/input/input_checkbox.jsx"];
|
|
39233
39682
|
const [checkboxRootProps, checkboxHostProps] = useCheckableProps(props);
|
|
39234
39683
|
const {
|
|
39235
39684
|
icon,
|
|
@@ -39351,7 +39800,7 @@ const CheckboxButtonStyleCSSVars = {
|
|
|
39351
39800
|
const CheckboxPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":checked", ":-navi-loading"];
|
|
39352
39801
|
const CheckboxPseudoElements = ["::-navi-loader", "::-navi-checkmark"];
|
|
39353
39802
|
|
|
39354
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
39803
|
+
installImportMetaCssBuild(import.meta);const css$F = /* css */`
|
|
39355
39804
|
@layer navi {
|
|
39356
39805
|
.navi_label {
|
|
39357
39806
|
--label-required-indicator-color: var(--navi-color-danger, #b42318);
|
|
@@ -39431,7 +39880,7 @@ installImportMetaCssBuild(import.meta);const css$E = /* css */`
|
|
|
39431
39880
|
* </Field>
|
|
39432
39881
|
*/
|
|
39433
39882
|
const Field = props => {
|
|
39434
|
-
import.meta.css = [css$
|
|
39883
|
+
import.meta.css = [css$F, "@jsenv/navi/src/control/field.jsx"];
|
|
39435
39884
|
const refDefault = useRef();
|
|
39436
39885
|
props.ref = props.ref || refDefault;
|
|
39437
39886
|
const {
|
|
@@ -39466,7 +39915,7 @@ const FieldCSSVars = {
|
|
|
39466
39915
|
spacingWithControl: "--spacing-with-control"
|
|
39467
39916
|
};
|
|
39468
39917
|
const FieldAsContainer = props => {
|
|
39469
|
-
import.meta.css = [css$
|
|
39918
|
+
import.meta.css = [css$F, "@jsenv/navi/src/control/field.jsx"];
|
|
39470
39919
|
const {
|
|
39471
39920
|
children
|
|
39472
39921
|
} = props;
|
|
@@ -39498,7 +39947,7 @@ const FieldAsContainer = props => {
|
|
|
39498
39947
|
};
|
|
39499
39948
|
const FIELD_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
|
|
39500
39949
|
const Label = props => {
|
|
39501
|
-
import.meta.css = [css$
|
|
39950
|
+
import.meta.css = [css$F, "@jsenv/navi/src/control/field.jsx"];
|
|
39502
39951
|
const {
|
|
39503
39952
|
children,
|
|
39504
39953
|
// Marks the label when its control is required. Takes what to show, or
|
|
@@ -39521,6 +39970,25 @@ const Label = props => {
|
|
|
39521
39970
|
if (!Object.hasOwn(props, "htmlFor") && controlId) {
|
|
39522
39971
|
props.htmlFor = controlId;
|
|
39523
39972
|
}
|
|
39973
|
+
// A label pointing at its control by id is not inside it and does not contain
|
|
39974
|
+
// it, so nothing in the DOM links the two — the control publishes its state
|
|
39975
|
+
// under that id and this is where the label picks it up (see
|
|
39976
|
+
// control_label_state.js). A label that wraps its control instead is told
|
|
39977
|
+
// through the navi_control_state event below.
|
|
39978
|
+
const {
|
|
39979
|
+
htmlFor
|
|
39980
|
+
} = props;
|
|
39981
|
+
useLayoutEffect(() => {
|
|
39982
|
+
if (!htmlFor) {
|
|
39983
|
+
return undefined;
|
|
39984
|
+
}
|
|
39985
|
+
return subscribeToControlState(htmlFor, controlState => {
|
|
39986
|
+
setConnected(Boolean(controlState));
|
|
39987
|
+
setDisabled(Boolean(controlState?.disabled));
|
|
39988
|
+
setReadOnly(Boolean(controlState?.readOnly));
|
|
39989
|
+
setRequired(Boolean(controlState?.required));
|
|
39990
|
+
});
|
|
39991
|
+
}, [htmlFor]);
|
|
39524
39992
|
const [messageProps, remainingProps] = extractMessageAndRemainingProps({
|
|
39525
39993
|
...props,
|
|
39526
39994
|
requiredIndicator: undefined
|
|
@@ -39641,7 +40109,7 @@ const InputSlot = ({
|
|
|
39641
40109
|
});
|
|
39642
40110
|
};
|
|
39643
40111
|
|
|
39644
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
40112
|
+
installImportMetaCssBuild(import.meta);const css$E = /* css */`
|
|
39645
40113
|
@layer navi {
|
|
39646
40114
|
.navi_radio {
|
|
39647
40115
|
--margin: 3px 3px 3px 5px;
|
|
@@ -40004,7 +40472,7 @@ const InputRadioHeadless = props => {
|
|
|
40004
40472
|
};
|
|
40005
40473
|
const APPEARANCE_SET = new Set(["icon", "button", "radio"]);
|
|
40006
40474
|
const InputRadioFieldInterface = props => {
|
|
40007
|
-
import.meta.css = [css$
|
|
40475
|
+
import.meta.css = [css$E, "@jsenv/navi/src/control/input/input_radio.jsx"];
|
|
40008
40476
|
const [radioRootProps, radioHostProps] = useCheckableProps(props);
|
|
40009
40477
|
const {
|
|
40010
40478
|
icon,
|
|
@@ -40150,7 +40618,7 @@ const RadioButtonStyleCSSVars = {
|
|
|
40150
40618
|
const RadioPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":checked", ":-navi-loading"];
|
|
40151
40619
|
const RadioPseudoElements = ["::-navi-loader", "::-navi-radiomark"];
|
|
40152
40620
|
|
|
40153
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
40621
|
+
installImportMetaCssBuild(import.meta);const css$D = /* css */`
|
|
40154
40622
|
@layer navi {
|
|
40155
40623
|
.navi_input_range {
|
|
40156
40624
|
--border-radius: 6px;
|
|
@@ -40405,7 +40873,7 @@ const InputRange = props => {
|
|
|
40405
40873
|
});
|
|
40406
40874
|
};
|
|
40407
40875
|
const InputRangeFieldInterface = props => {
|
|
40408
|
-
import.meta.css = [css$
|
|
40876
|
+
import.meta.css = [css$D, "@jsenv/navi/src/control/input/input_range.jsx"];
|
|
40409
40877
|
const {
|
|
40410
40878
|
ref
|
|
40411
40879
|
} = props;
|
|
@@ -42332,7 +42800,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
42332
42800
|
* This means an editable thing MUST have a parent with position relative that wraps the content and the eventual editable input
|
|
42333
42801
|
*
|
|
42334
42802
|
*/
|
|
42335
|
-
const css$
|
|
42803
|
+
const css$C = /* css */`
|
|
42336
42804
|
.navi_editable_wrapper {
|
|
42337
42805
|
--inset-top: 0px;
|
|
42338
42806
|
--inset-right: 0px;
|
|
@@ -42381,7 +42849,7 @@ const useEditionController = () => {
|
|
|
42381
42849
|
};
|
|
42382
42850
|
};
|
|
42383
42851
|
const Editable = props => {
|
|
42384
|
-
import.meta.css = [css$
|
|
42852
|
+
import.meta.css = [css$C, "@jsenv/navi/src/control/edition/editable.jsx"];
|
|
42385
42853
|
let {
|
|
42386
42854
|
children,
|
|
42387
42855
|
action,
|
|
@@ -42611,6 +43079,7 @@ const useFormGroup = props => {
|
|
|
42611
43079
|
// against the state of the previous frame.
|
|
42612
43080
|
uiStateController.shouldRequestAction = value => Boolean(props.canSendWhileUnchanged) || !compareTwoJsValues(withoutEmptyFields(value), uiStateController.sentUIState);
|
|
42613
43081
|
useFirstUIStateAsSent(uiStateController);
|
|
43082
|
+
useUnregisteredControlWarning(props.ref);
|
|
42614
43083
|
const {
|
|
42615
43084
|
basePseudoState,
|
|
42616
43085
|
children
|
|
@@ -42759,6 +43228,15 @@ const useFirstUIStateAsSent = uiStateController => {
|
|
|
42759
43228
|
uiStateController.sentUIState = readHeldUIState(uiStateController);
|
|
42760
43229
|
}, [uiStateController]);
|
|
42761
43230
|
};
|
|
43231
|
+
const useUnregisteredControlWarning = ref => {
|
|
43232
|
+
// No dependency array: fields appear and disappear as the form re-renders,
|
|
43233
|
+
// and a field rendered later is exactly the one worth catching.
|
|
43234
|
+
useLayoutEffect(() => {
|
|
43235
|
+
{
|
|
43236
|
+
return;
|
|
43237
|
+
}
|
|
43238
|
+
});
|
|
43239
|
+
};
|
|
42762
43240
|
const FormPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
|
|
42763
43241
|
|
|
42764
43242
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Constraint_validation
|
|
@@ -42795,7 +43273,7 @@ HTMLFormElement.prototype.requestSubmit = function (submitter) {
|
|
|
42795
43273
|
// form.dispatchEvent(customEvent);
|
|
42796
43274
|
// };
|
|
42797
43275
|
|
|
42798
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
43276
|
+
installImportMetaCssBuild(import.meta);const css$B = /* css */`
|
|
42799
43277
|
.navi_group {
|
|
42800
43278
|
--group-border-width: 1px;
|
|
42801
43279
|
|
|
@@ -42891,7 +43369,7 @@ const Group = ({
|
|
|
42891
43369
|
vertical = row,
|
|
42892
43370
|
...props
|
|
42893
43371
|
}) => {
|
|
42894
|
-
import.meta.css = [css$
|
|
43372
|
+
import.meta.css = [css$B, "@jsenv/navi/src/control/group.jsx"];
|
|
42895
43373
|
return jsx(Box, {
|
|
42896
43374
|
baseClassName: "navi_group",
|
|
42897
43375
|
"data-vertical": vertical ? "" : undefined,
|
|
@@ -43018,7 +43496,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
43018
43496
|
* pair. One popup holding slides of its own contents has no such problem — and
|
|
43019
43497
|
* it is the same component in the document, in a dialog or in a popover.
|
|
43020
43498
|
*/
|
|
43021
|
-
const css$
|
|
43499
|
+
const css$A = /* css */`
|
|
43022
43500
|
/* Every slide in the same grid cell: the box then measures itself on the
|
|
43023
43501
|
LARGEST of them, in both directions, without anything being measured by
|
|
43024
43502
|
hand — which is also why nothing here resizes as the slides change. Each
|
|
@@ -43328,7 +43806,7 @@ const SlideContainer = ({
|
|
|
43328
43806
|
children,
|
|
43329
43807
|
...rest
|
|
43330
43808
|
}) => {
|
|
43331
|
-
import.meta.css = [css$
|
|
43809
|
+
import.meta.css = [css$A, "@jsenv/navi/src/layout/slide_container.jsx"];
|
|
43332
43810
|
const debugFocus = useDebugFocus();
|
|
43333
43811
|
const trackRef = useRef();
|
|
43334
43812
|
// The box itself: it is what takes the keyboard when what is on screen holds
|
|
@@ -45192,7 +45670,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
45192
45670
|
* pass through untouched via `...rest` to whichever of Popover/Dialog
|
|
45193
45671
|
* actually renders.
|
|
45194
45672
|
*/
|
|
45195
|
-
const css$
|
|
45673
|
+
const css$z = /* css */`
|
|
45196
45674
|
@layer navi {
|
|
45197
45675
|
.navi_popup {
|
|
45198
45676
|
--popup-border-radius: var(--navi-popup-border-radius);
|
|
@@ -45283,10 +45761,15 @@ const css$y = /* css */`
|
|
|
45283
45761
|
* @param {string} [props.className] - Merged with the shared
|
|
45284
45762
|
* `"navi_popup"` class (see this file's own CSS) rather than replacing
|
|
45285
45763
|
* it.
|
|
45764
|
+
* @param {boolean} [props.mountWhenClosed] - Builds `children` right away
|
|
45765
|
+
* instead of waiting for the first open (see popup_content_mount.js). For
|
|
45766
|
+
* content something depends on while the popup is still closed: a value read
|
|
45767
|
+
* off it, fields a surrounding form collects on submit, a size measured from
|
|
45768
|
+
* outside.
|
|
45286
45769
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
45287
45770
|
*/
|
|
45288
45771
|
const Popup = props => {
|
|
45289
|
-
import.meta.css = [css$
|
|
45772
|
+
import.meta.css = [css$z, "@jsenv/navi/src/layout/popup.jsx"];
|
|
45290
45773
|
const {
|
|
45291
45774
|
mode: modeProp,
|
|
45292
45775
|
maxWidth,
|
|
@@ -45429,7 +45912,7 @@ const commitSubtree = (controller, e) => {
|
|
|
45429
45912
|
}
|
|
45430
45913
|
};
|
|
45431
45914
|
|
|
45432
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
45915
|
+
installImportMetaCssBuild(import.meta);const css$y = /* css */`
|
|
45433
45916
|
.navi_picker {
|
|
45434
45917
|
/* Sizing ceilings (maxmax), background, box-shadow, outline, padding,
|
|
45435
45918
|
overflow... are already handled correctly by Popup/Popover/Dialog
|
|
@@ -45541,7 +46024,7 @@ installImportMetaCssBuild(import.meta);const css$x = /* css */`
|
|
|
45541
46024
|
}
|
|
45542
46025
|
`;
|
|
45543
46026
|
const PickerCustomResolver = props => {
|
|
45544
|
-
import.meta.css = [css$
|
|
46027
|
+
import.meta.css = [css$y, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
|
|
45545
46028
|
if (props.children === undefined) {
|
|
45546
46029
|
return jsx(PickerNative, {
|
|
45547
46030
|
...props
|
|
@@ -45807,6 +46290,13 @@ const PickerCustom = props => {
|
|
|
45807
46290
|
Object.assign(popupProps, {
|
|
45808
46291
|
anchor: props.ref,
|
|
45809
46292
|
openController,
|
|
46293
|
+
// A picker whose value was never given to it reads it off the control in
|
|
46294
|
+
// its popup (see useUIFacadeStateController): the trigger shows what the
|
|
46295
|
+
// list inside says is selected, so that list has to exist before anyone
|
|
46296
|
+
// opens anything. Told a value — even an empty one — the picker owns it
|
|
46297
|
+
// and pushes it down instead, leaving the popup free to build its
|
|
46298
|
+
// content only when it is first opened (see popup_content_mount.js).
|
|
46299
|
+
mountWhenClosed: !Object.hasOwn(props, "value") && !Object.hasOwn(props, "defaultValue"),
|
|
45810
46300
|
// Not on pickerProps (the trigger): commands.js's own
|
|
45811
46301
|
// resolveClosestExpandable() does `el.closest("[aria-expanded]")` to
|
|
45812
46302
|
// find where to dispatch navi_request_open/navi_request_close — and
|
|
@@ -46186,7 +46676,7 @@ const LoadingIndicator = ({
|
|
|
46186
46676
|
});
|
|
46187
46677
|
};
|
|
46188
46678
|
|
|
46189
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
46679
|
+
installImportMetaCssBuild(import.meta);const css$x = /* css */`
|
|
46190
46680
|
@layer navi {
|
|
46191
46681
|
.navi_separator {
|
|
46192
46682
|
--size: 1px;
|
|
@@ -46264,7 +46754,7 @@ const Separator = ({
|
|
|
46264
46754
|
style,
|
|
46265
46755
|
...props
|
|
46266
46756
|
}) => {
|
|
46267
|
-
import.meta.css = [css$
|
|
46757
|
+
import.meta.css = [css$x, "@jsenv/navi/src/layout/separator.jsx"];
|
|
46268
46758
|
return jsx(Box, {
|
|
46269
46759
|
as: vertical ? "span" : "hr",
|
|
46270
46760
|
...props,
|
|
@@ -46757,7 +47247,7 @@ const ListItemFooter = props => {
|
|
|
46757
47247
|
});
|
|
46758
47248
|
};
|
|
46759
47249
|
|
|
46760
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
47250
|
+
installImportMetaCssBuild(import.meta);const css$w = /* css */`
|
|
46761
47251
|
@layer navi {
|
|
46762
47252
|
.navi_list_container[navi-selectable] {
|
|
46763
47253
|
/* Focus outline */
|
|
@@ -46961,7 +47451,7 @@ const ListSelectableResolver = props => {
|
|
|
46961
47451
|
};
|
|
46962
47452
|
const ListSelectable = props => {
|
|
46963
47453
|
const Next = useNextResolver();
|
|
46964
|
-
import.meta.css = [css$
|
|
47454
|
+
import.meta.css = [css$w, "@jsenv/navi/src/control/list/list_selectable.jsx"];
|
|
46965
47455
|
// we allow ourselves to auto-generate a name
|
|
46966
47456
|
const defaultName = useId();
|
|
46967
47457
|
props.name = props.name || `listbox_${defaultName}`;
|
|
@@ -47556,7 +48046,7 @@ const ListVirtualContext = createContext(null);
|
|
|
47556
48046
|
// that returning a component of one's own — instead of a bare <List.Item> —
|
|
47557
48047
|
// works the same way.
|
|
47558
48048
|
const ListRowContext = createContext(null);
|
|
47559
|
-
const css$
|
|
48049
|
+
const css$v = /* css */`
|
|
47560
48050
|
@layer navi {
|
|
47561
48051
|
.navi_list_container {
|
|
47562
48052
|
--list-outline-width: 1px;
|
|
@@ -48009,13 +48499,10 @@ const css$u = /* css */`
|
|
|
48009
48499
|
font-size: 1em;
|
|
48010
48500
|
line-height: 1.4;
|
|
48011
48501
|
}
|
|
48012
|
-
/*
|
|
48013
|
-
|
|
48014
|
-
|
|
48015
|
-
|
|
48016
|
-
and disappear as things load. Targeted on the outline itself rather than
|
|
48017
|
-
inherited from the item, so a control nested deeper (which has room around
|
|
48018
|
-
it, and does not reach the edges) keeps the outline it asked for. */
|
|
48502
|
+
/* Same rule as [data-scrollable] in box.jsx, said again for this scroller:
|
|
48503
|
+
what an item holds IS against the edge of the scroll container — the list
|
|
48504
|
+
element between the two is markup, not spacing — so its loading outline
|
|
48505
|
+
stays inside its own box rather than raising a scrollbar. */
|
|
48019
48506
|
.navi_list_item > .navi_loading_outline_wrapper,
|
|
48020
48507
|
.navi_list_item > * > .navi_loading_outline_wrapper,
|
|
48021
48508
|
.navi_list_item_header > * > .navi_loading_outline_wrapper,
|
|
@@ -48084,7 +48571,7 @@ const css$u = /* css */`
|
|
|
48084
48571
|
}
|
|
48085
48572
|
`;
|
|
48086
48573
|
const ListUI = props => {
|
|
48087
|
-
import.meta.css = [css$
|
|
48574
|
+
import.meta.css = [css$v, "@jsenv/navi/src/control/list/list.jsx"];
|
|
48088
48575
|
const {
|
|
48089
48576
|
ref,
|
|
48090
48577
|
renderBudget: renderBudgetProp = RENDER_BUDGET_DEFAULT,
|
|
@@ -51187,7 +51674,7 @@ const PickerPresetResolver = props => {
|
|
|
51187
51674
|
});
|
|
51188
51675
|
};
|
|
51189
51676
|
|
|
51190
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
51677
|
+
installImportMetaCssBuild(import.meta);const css$u = /* css */`
|
|
51191
51678
|
@layer navi {
|
|
51192
51679
|
}
|
|
51193
51680
|
.navi_badge {
|
|
@@ -51299,7 +51786,7 @@ const Badge = ({
|
|
|
51299
51786
|
className,
|
|
51300
51787
|
...props
|
|
51301
51788
|
}) => {
|
|
51302
|
-
import.meta.css = [css$
|
|
51789
|
+
import.meta.css = [css$u, "@jsenv/navi/src/text/badge.jsx"];
|
|
51303
51790
|
const defaultRef = useRef();
|
|
51304
51791
|
props.ref = props.ref || defaultRef;
|
|
51305
51792
|
const {
|
|
@@ -51351,7 +51838,7 @@ const BadgeButton = props => {
|
|
|
51351
51838
|
};
|
|
51352
51839
|
Badge.Button = BadgeButton;
|
|
51353
51840
|
|
|
51354
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
51841
|
+
installImportMetaCssBuild(import.meta);const css$t = /* css */`
|
|
51355
51842
|
@layer navi {
|
|
51356
51843
|
}
|
|
51357
51844
|
.navi_badge_list {
|
|
@@ -51376,7 +51863,7 @@ const BadgeList = ({
|
|
|
51376
51863
|
max,
|
|
51377
51864
|
...props
|
|
51378
51865
|
}) => {
|
|
51379
|
-
import.meta.css = [css$
|
|
51866
|
+
import.meta.css = [css$t, "@jsenv/navi/src/text/badge_list.jsx"];
|
|
51380
51867
|
const measureRef = useRef();
|
|
51381
51868
|
const visibleRef = useRef();
|
|
51382
51869
|
useLayoutEffect(() => {
|
|
@@ -51451,7 +51938,7 @@ const BadgeList = ({
|
|
|
51451
51938
|
});
|
|
51452
51939
|
};
|
|
51453
51940
|
|
|
51454
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
51941
|
+
installImportMetaCssBuild(import.meta);const css$s = /* css */`
|
|
51455
51942
|
.navi_color {
|
|
51456
51943
|
display: block;
|
|
51457
51944
|
aspect-ratio: 1/1;
|
|
@@ -51482,7 +51969,7 @@ const Color = ({
|
|
|
51482
51969
|
children,
|
|
51483
51970
|
...rest
|
|
51484
51971
|
}) => {
|
|
51485
|
-
import.meta.css = [css$
|
|
51972
|
+
import.meta.css = [css$s, "@jsenv/navi/src/text/color.jsx"];
|
|
51486
51973
|
const color = children || undefined;
|
|
51487
51974
|
return jsx(Box, {
|
|
51488
51975
|
as: "span",
|
|
@@ -51937,7 +52424,7 @@ const PickerFileUI = () => {
|
|
|
51937
52424
|
return String(value);
|
|
51938
52425
|
};
|
|
51939
52426
|
|
|
51940
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
52427
|
+
installImportMetaCssBuild(import.meta);const css$r = /* css */`
|
|
51941
52428
|
@layer navi {
|
|
51942
52429
|
.navi_picker {
|
|
51943
52430
|
--picker-border-radius: var(--navi-control-border-radius);
|
|
@@ -52263,7 +52750,7 @@ installImportMetaCssBuild(import.meta);const css$q = /* css */`
|
|
|
52263
52750
|
}
|
|
52264
52751
|
`;
|
|
52265
52752
|
const PickerButton = props => {
|
|
52266
|
-
import.meta.css = [css$
|
|
52753
|
+
import.meta.css = [css$r, "@jsenv/navi/src/control/picker/picker.jsx"];
|
|
52267
52754
|
if (typeof props.maxLines === "string") {
|
|
52268
52755
|
props.maxLines = parseInt(props.maxLines);
|
|
52269
52756
|
}
|
|
@@ -52726,7 +53213,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
52726
53213
|
* refuse it on purpose, which is what keeps the focus where the travel happens
|
|
52727
53214
|
* instead of moving it into a slide that is about to leave.
|
|
52728
53215
|
*/
|
|
52729
|
-
const css$
|
|
53216
|
+
const css$q = /* css */`
|
|
52730
53217
|
@layer navi {
|
|
52731
53218
|
.navi_picker_spin {
|
|
52732
53219
|
/* A picker one steps through is still a picker: what themes every picker
|
|
@@ -53072,7 +53559,7 @@ const Spin = ({
|
|
|
53072
53559
|
nextLabel,
|
|
53073
53560
|
...rest
|
|
53074
53561
|
}) => {
|
|
53075
|
-
import.meta.css = [css$
|
|
53562
|
+
import.meta.css = [css$q, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
|
|
53076
53563
|
const id = useId();
|
|
53077
53564
|
const containerId = `${id}_values`;
|
|
53078
53565
|
const controlId = `${id}_control`;
|
|
@@ -53683,7 +54170,7 @@ const addDays = (day, count) => {
|
|
|
53683
54170
|
};
|
|
53684
54171
|
|
|
53685
54172
|
installImportMetaCssBuild(import.meta);// TOFIX: select in data then reset, it reset to red/blue instead of red/blue/green
|
|
53686
|
-
const css$
|
|
54173
|
+
const css$p = /* css */`
|
|
53687
54174
|
.navi_checkbox_group {
|
|
53688
54175
|
border-style: solid;
|
|
53689
54176
|
|
|
@@ -53703,7 +54190,7 @@ const CheckboxGroup = props => {
|
|
|
53703
54190
|
return checkboxGroup;
|
|
53704
54191
|
};
|
|
53705
54192
|
const CheckboxGroupInterface = props => {
|
|
53706
|
-
import.meta.css = [css$
|
|
54193
|
+
import.meta.css = [css$p, "@jsenv/navi/src/control/input/checkbox_group.jsx"];
|
|
53707
54194
|
const {
|
|
53708
54195
|
ref
|
|
53709
54196
|
} = props;
|
|
@@ -53752,7 +54239,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
53752
54239
|
* shared sheet is registered here too — a page may render a Textarea without
|
|
53753
54240
|
* any Input.
|
|
53754
54241
|
*/
|
|
53755
|
-
const css$
|
|
54242
|
+
const css$o = /* css */`
|
|
53756
54243
|
.navi_input.navi_textarea {
|
|
53757
54244
|
.navi_control_input {
|
|
53758
54245
|
min-height: calc(var(--textarea-min-rows, 1.5) * 1lh);
|
|
@@ -53769,6 +54256,18 @@ const css$n = /* css */`
|
|
|
53769
54256
|
/* The control grows itself; resizable below hands the handle back. */
|
|
53770
54257
|
resize: none;
|
|
53771
54258
|
overflow: auto;
|
|
54259
|
+
/* A placeholder must be readable in full before anything is typed: a
|
|
54260
|
+
field that opens already scrolled reads as a field that already has
|
|
54261
|
+
text in it. Its wrapped height is measured (see usePlaceholderHeight)
|
|
54262
|
+
because it only exists once laid out, and it only raises the floor
|
|
54263
|
+
while the placeholder is what is being shown — what is typed sizes the
|
|
54264
|
+
box on its own. */
|
|
54265
|
+
&:placeholder-shown {
|
|
54266
|
+
min-height: max(
|
|
54267
|
+
calc(var(--textarea-min-rows, 1.5) * 1lh),
|
|
54268
|
+
var(--x-textarea-placeholder-height, 0px)
|
|
54269
|
+
);
|
|
54270
|
+
}
|
|
53772
54271
|
}
|
|
53773
54272
|
&[data-resizable] .navi_control_input {
|
|
53774
54273
|
height: calc(var(--textarea-min-rows, 1.5) * 1lh);
|
|
@@ -53806,7 +54305,9 @@ const css$n = /* css */`
|
|
|
53806
54305
|
* @param {number} [maxRows] Lines after which the control stops growing and
|
|
53807
54306
|
* scrolls instead. Without it the control grows with its content.
|
|
53808
54307
|
* @param {boolean} [resizable] Give the browser's vertical resize handle back.
|
|
53809
|
-
*
|
|
54308
|
+
* An exchange, not an addition: the hand takes over from the automatic
|
|
54309
|
+
* growth, so the control stops following what is typed and stays at the
|
|
54310
|
+
* height it was last dragged to (starting at `minRows`).
|
|
53810
54311
|
* @param {number} [maxLength] The character limit, validated at submit. Pair
|
|
53811
54312
|
* with `maxLengthGuard` to block typing past it, and render a
|
|
53812
54313
|
* TextareaCharCount to show it.
|
|
@@ -53825,9 +54326,10 @@ const Textarea = ({
|
|
|
53825
54326
|
width = "35ch",
|
|
53826
54327
|
...props
|
|
53827
54328
|
}) => {
|
|
53828
|
-
import.meta.css = [inputCss + css$
|
|
54329
|
+
import.meta.css = [inputCss + css$o, "@jsenv/navi/src/control/input/textarea.jsx"];
|
|
53829
54330
|
const defaultRef = useRef(null);
|
|
53830
54331
|
props.ref = props.ref || defaultRef;
|
|
54332
|
+
usePlaceholderHeight(props.ref, props.placeholder);
|
|
53831
54333
|
const [rootProps, hostProps, childrenWrapperProps] = useControlProps(props, {
|
|
53832
54334
|
controlType: "input"
|
|
53833
54335
|
});
|
|
@@ -53898,7 +54400,7 @@ const TextareaCharCount = ({
|
|
|
53898
54400
|
maxLength,
|
|
53899
54401
|
...rest
|
|
53900
54402
|
}) => {
|
|
53901
|
-
import.meta.css = [css$
|
|
54403
|
+
import.meta.css = [css$o, "@jsenv/navi/src/control/input/textarea.jsx"];
|
|
53902
54404
|
const resolvedValue = signal ? signal.value : value;
|
|
53903
54405
|
const length = typeof resolvedValue === "string" ? resolvedValue.length : 0;
|
|
53904
54406
|
return jsx(Box, {
|
|
@@ -53908,6 +54410,61 @@ const TextareaCharCount = ({
|
|
|
53908
54410
|
children: maxLength === undefined ? length : `${length}/${maxLength}`
|
|
53909
54411
|
});
|
|
53910
54412
|
};
|
|
54413
|
+
|
|
54414
|
+
// `field-sizing: content` sizes the box from the value, and an empty field has
|
|
54415
|
+
// none — the placeholder is text the browser refuses to make room for. So the
|
|
54416
|
+
// height it wraps to is measured and published as --x-textarea-placeholder-height
|
|
54417
|
+
// for the CSS above to use as a floor.
|
|
54418
|
+
const usePlaceholderHeight = (ref, placeholder) => {
|
|
54419
|
+
useLayoutEffect(() => {
|
|
54420
|
+
const textareaEl = ref.current;
|
|
54421
|
+
if (!placeholder) {
|
|
54422
|
+
textareaEl.style.removeProperty("--x-textarea-placeholder-height");
|
|
54423
|
+
return null;
|
|
54424
|
+
}
|
|
54425
|
+
let widthMeasured;
|
|
54426
|
+
const measure = () => {
|
|
54427
|
+
// What is typed sizes the box itself; the placeholder is not displayed
|
|
54428
|
+
// then, and scrollHeight would report the value's height instead.
|
|
54429
|
+
if (textareaEl.value !== "") {
|
|
54430
|
+
return;
|
|
54431
|
+
}
|
|
54432
|
+
const {
|
|
54433
|
+
paddingTop,
|
|
54434
|
+
paddingBottom
|
|
54435
|
+
} = getComputedStyle(textareaEl);
|
|
54436
|
+
// Cleared before reading: scrollHeight can never report less than the
|
|
54437
|
+
// height already applied, so measuring on top of a previous measure could
|
|
54438
|
+
// only ever grow the box, never let it shrink back on a wider viewport.
|
|
54439
|
+
textareaEl.style.setProperty("--x-textarea-placeholder-height", "0px");
|
|
54440
|
+
const contentHeight = textareaEl.scrollHeight - parseFloat(paddingTop) - parseFloat(paddingBottom);
|
|
54441
|
+
widthMeasured = textareaEl.clientWidth;
|
|
54442
|
+
textareaEl.style.setProperty("--x-textarea-placeholder-height", `${contentHeight}px`);
|
|
54443
|
+
};
|
|
54444
|
+
measure();
|
|
54445
|
+
// The placeholder wraps against the available width, so a new width is a
|
|
54446
|
+
// new number of lines. Height changes are ignored: this measure is what
|
|
54447
|
+
// causes them, and reacting to them would be reacting to ourselves.
|
|
54448
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
54449
|
+
if (textareaEl.clientWidth !== widthMeasured) {
|
|
54450
|
+
measure();
|
|
54451
|
+
}
|
|
54452
|
+
});
|
|
54453
|
+
resizeObserver.observe(textareaEl);
|
|
54454
|
+
// The width may have changed while the field held a value, when measuring
|
|
54455
|
+
// was impossible — emptying it is when the placeholder comes back.
|
|
54456
|
+
const onInput = () => {
|
|
54457
|
+
if (textareaEl.value === "") {
|
|
54458
|
+
measure();
|
|
54459
|
+
}
|
|
54460
|
+
};
|
|
54461
|
+
textareaEl.addEventListener("input", onInput);
|
|
54462
|
+
return () => {
|
|
54463
|
+
resizeObserver.disconnect();
|
|
54464
|
+
textareaEl.removeEventListener("input", onInput);
|
|
54465
|
+
};
|
|
54466
|
+
}, [placeholder]);
|
|
54467
|
+
};
|
|
53911
54468
|
const RealTextarea = ({
|
|
53912
54469
|
maxLength,
|
|
53913
54470
|
...domProps
|
|
@@ -54233,7 +54790,7 @@ const isTextInputElement = (el) => {
|
|
|
54233
54790
|
);
|
|
54234
54791
|
};
|
|
54235
54792
|
|
|
54236
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
54793
|
+
installImportMetaCssBuild(import.meta);const css$n = /* css */`
|
|
54237
54794
|
.navi_input_duration {
|
|
54238
54795
|
--duration-separator-spacing: 4px;
|
|
54239
54796
|
--loader-color: var(--navi-loader-color);
|
|
@@ -54300,7 +54857,7 @@ installImportMetaCssBuild(import.meta);const css$m = /* css */`
|
|
|
54300
54857
|
* "auto" aligns each field toward its neighbouring separator (first→right, last→left, middle/solo→center).
|
|
54301
54858
|
*/
|
|
54302
54859
|
const InputDuration = props => {
|
|
54303
|
-
import.meta.css = [css$
|
|
54860
|
+
import.meta.css = [css$n, "@jsenv/navi/src/control/input/input_duration.jsx"];
|
|
54304
54861
|
const defaultRef = useRef();
|
|
54305
54862
|
props.ref = props.ref || defaultRef;
|
|
54306
54863
|
props.max = props.max || "23h59";
|
|
@@ -54802,7 +55359,7 @@ const InputDurationPart = ({
|
|
|
54802
55359
|
});
|
|
54803
55360
|
};
|
|
54804
55361
|
|
|
54805
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
55362
|
+
installImportMetaCssBuild(import.meta);const css$m = /* css */`
|
|
54806
55363
|
.navi_radio_group {
|
|
54807
55364
|
border-style: solid;
|
|
54808
55365
|
|
|
@@ -54822,7 +55379,7 @@ const RadioGroup = props => {
|
|
|
54822
55379
|
return radioGroup;
|
|
54823
55380
|
};
|
|
54824
55381
|
const RadioGroupInterface = props => {
|
|
54825
|
-
import.meta.css = [css$
|
|
55382
|
+
import.meta.css = [css$m, "@jsenv/navi/src/control/input/radio_group.jsx"];
|
|
54826
55383
|
const {
|
|
54827
55384
|
ref
|
|
54828
55385
|
} = props;
|
|
@@ -54851,6 +55408,139 @@ const RadioGroupInterface = props => {
|
|
|
54851
55408
|
});
|
|
54852
55409
|
};
|
|
54853
55410
|
|
|
55411
|
+
installImportMetaCssBuild(import.meta);/**
|
|
55412
|
+
* A native `<select>` that is a navi control: its value enters the state of the
|
|
55413
|
+
* `<Form>` around it, and it takes `signal`, `uiAction`, `action`, `command`,
|
|
55414
|
+
* `value`/`defaultValue`, `readOnly`, `disabled`, `required` like every other
|
|
55415
|
+
* control.
|
|
55416
|
+
*
|
|
55417
|
+
* Native on purpose, and not a Picker: on a phone a `<select>` opens the
|
|
55418
|
+
* system's own full-screen list — the thing the thumb handles best and the user
|
|
55419
|
+
* already knows — with no popup, no positioning and no focus trap to get wrong.
|
|
55420
|
+
* That is the right control for a short closed list (a gender, an age bracket,
|
|
55421
|
+
* "who sees this"). A long, searchable list with rich content in its options is
|
|
55422
|
+
* Picker's problem, not this one.
|
|
55423
|
+
*
|
|
55424
|
+
* The options are the children, written as HTML: an `<optgroup>`, a `disabled`
|
|
55425
|
+
* option, an `<hr>` between two groups need no support from this component.
|
|
55426
|
+
*
|
|
55427
|
+
* Two things the component absorbs, both traps met before:
|
|
55428
|
+
* - a native `<select>` ignores `defaultValue` and reads `selected` off its
|
|
55429
|
+
* options instead; here `value`/`defaultValue` mean what they mean everywhere
|
|
55430
|
+
* else in navi, and the element is told what to show.
|
|
55431
|
+
* - a native `<select>` has no `readonly`; `readOnly` here refuses the
|
|
55432
|
+
* interaction (the list does not open, a change from the keyboard is put
|
|
55433
|
+
* back) and says so with `aria-readonly`.
|
|
55434
|
+
*
|
|
55435
|
+
* Styled as a `.navi_input` box so a select and an input sitting next to each
|
|
55436
|
+
* other are the same box. `appearance: none` only changes how the closed
|
|
55437
|
+
* control is drawn — the list it opens stays the platform's own, which is the
|
|
55438
|
+
* whole point of using a select.
|
|
55439
|
+
*/
|
|
55440
|
+
const css$l = /* css */`
|
|
55441
|
+
.navi_input.navi_select {
|
|
55442
|
+
.navi_control_input {
|
|
55443
|
+
/* Room for the chevron, which sits over the padding rather than beside
|
|
55444
|
+
the control — anything beside it would be a click that misses. */
|
|
55445
|
+
padding-right: calc(var(--x-padding-right) + 1em);
|
|
55446
|
+
/* A form control keeps a line of its own whatever the page is written in,
|
|
55447
|
+
and lh units elsewhere in the box are resolved against a real number. */
|
|
55448
|
+
line-height: normal;
|
|
55449
|
+
/* The closed control is drawn by us so it matches the other fields; the
|
|
55450
|
+
list it opens is untouched and stays the system's. */
|
|
55451
|
+
appearance: none;
|
|
55452
|
+
cursor: pointer;
|
|
55453
|
+
}
|
|
55454
|
+
&[data-readonly] .navi_control_input,
|
|
55455
|
+
&[data-disabled] .navi_control_input {
|
|
55456
|
+
cursor: inherit;
|
|
55457
|
+
}
|
|
55458
|
+
|
|
55459
|
+
.navi_select_arrow {
|
|
55460
|
+
position: absolute;
|
|
55461
|
+
top: 50%;
|
|
55462
|
+
right: var(--x-padding-right);
|
|
55463
|
+
display: flex;
|
|
55464
|
+
color: var(--color-dimmed);
|
|
55465
|
+
translate: 0 -50%;
|
|
55466
|
+
/* The arrow is drawn on top of the control it belongs to: a click on it
|
|
55467
|
+
must reach the select and open the list. */
|
|
55468
|
+
pointer-events: none;
|
|
55469
|
+
}
|
|
55470
|
+
}
|
|
55471
|
+
`;
|
|
55472
|
+
|
|
55473
|
+
/**
|
|
55474
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
55475
|
+
* value?: string,
|
|
55476
|
+
* defaultValue?: string,
|
|
55477
|
+
* signal?: import("@preact/signals").Signal<string>,
|
|
55478
|
+
* name?: string,
|
|
55479
|
+
* width?: string,
|
|
55480
|
+
* [key: string]: any,
|
|
55481
|
+
* }>}
|
|
55482
|
+
* @param {string} [value] The choice the control is GIVEN — what is already
|
|
55483
|
+
* saved. A form holding it considers that field as already sent.
|
|
55484
|
+
* @param {string} [defaultValue] The choice the control PROPOSES, and what a
|
|
55485
|
+
* reset goes back to. Sending it back is an answer, so a form counts it as
|
|
55486
|
+
* something to send.
|
|
55487
|
+
* @param {string} [width] The control's width. Left out, the box takes the
|
|
55488
|
+
* width of its widest option.
|
|
55489
|
+
*/
|
|
55490
|
+
const Select = ({
|
|
55491
|
+
width,
|
|
55492
|
+
multiple,
|
|
55493
|
+
...props
|
|
55494
|
+
}) => {
|
|
55495
|
+
import.meta.css = [inputCss + css$l, "@jsenv/navi/src/control/input/select.jsx"];
|
|
55496
|
+
const defaultRef = useRef(null);
|
|
55497
|
+
props.ref = props.ref || defaultRef;
|
|
55498
|
+
seedDefaultValueFromSignal(props);
|
|
55499
|
+
const [rootProps, hostProps] = useControlProps(props, {
|
|
55500
|
+
controlType: "select"
|
|
55501
|
+
});
|
|
55502
|
+
const {
|
|
55503
|
+
basePseudoState
|
|
55504
|
+
} = hostProps;
|
|
55505
|
+
// `type` on a <select> is the browser's own read-only "select-one".
|
|
55506
|
+
delete hostProps.type;
|
|
55507
|
+
const loading = basePseudoState[":-navi-loading"];
|
|
55508
|
+
if (width !== undefined) {
|
|
55509
|
+
// On the select, not on the box around it: the box is fit-content and the
|
|
55510
|
+
// control is what has a width to give.
|
|
55511
|
+
hostProps.width = width;
|
|
55512
|
+
}
|
|
55513
|
+
return jsxs(Box, {
|
|
55514
|
+
as: "span",
|
|
55515
|
+
inline: true,
|
|
55516
|
+
flex: true,
|
|
55517
|
+
baseClassName: "navi_input",
|
|
55518
|
+
className: "navi_select",
|
|
55519
|
+
...rootProps,
|
|
55520
|
+
basePseudoState: basePseudoState,
|
|
55521
|
+
styleCSSVars: InputStyleCSSVars,
|
|
55522
|
+
pseudoStateSelector: ".navi_control_input",
|
|
55523
|
+
pseudoClasses: InputPseudoClasses,
|
|
55524
|
+
pseudoElements: InputPseudoElements,
|
|
55525
|
+
"data-callout-anchor": ".navi_control_input",
|
|
55526
|
+
children: [jsx(LoadingOutline, {
|
|
55527
|
+
loading: loading,
|
|
55528
|
+
color: "var(--loader-color)",
|
|
55529
|
+
inset: -1
|
|
55530
|
+
}), jsx(Box, {
|
|
55531
|
+
...hostProps,
|
|
55532
|
+
as: "select",
|
|
55533
|
+
baseClassName: "navi_control_input"
|
|
55534
|
+
}), jsx("span", {
|
|
55535
|
+
className: "navi_select_arrow",
|
|
55536
|
+
children: jsx(Icon, {
|
|
55537
|
+
lineOverflow: "allow",
|
|
55538
|
+
children: jsx(ChevronDownSvg$1, {})
|
|
55539
|
+
})
|
|
55540
|
+
})]
|
|
55541
|
+
});
|
|
55542
|
+
};
|
|
55543
|
+
|
|
54854
55544
|
/**
|
|
54855
55545
|
* applySearch — matches value against searchText.
|
|
54856
55546
|
*
|
|
@@ -63433,5 +64123,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
63433
64123
|
})
|
|
63434
64124
|
});
|
|
63435
64125
|
|
|
63436
|
-
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, 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, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineNaviConfirmPopupOptions, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, 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, valueInLocalStorage, windowWidthSignal };
|
|
64126
|
+
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, 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, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineNaviConfirmPopupOptions, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, 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, valueInLocalStorage, windowWidthSignal };
|
|
63437
64127
|
//# sourceMappingURL=jsenv_navi.js.map
|