@jsenv/navi 0.29.109 → 0.29.111
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 +653 -155
- package/dist/jsenv_navi.js.map +51 -40
- package/docs/AI_INSTRUCTIONS.md +16 -2
- package/docs/actions.md +7 -0
- package/docs/control_group.md +9 -0
- package/docs/error_handling.md +17 -0
- package/docs/field_validation.md +193 -0
- package/docs/popup_open.md +9 -2
- package/docs/typography.md +38 -3
- package/package.json +3 -3
package/dist/jsenv_navi.js
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
6
6
|
export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
|
|
7
|
-
import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, initFocusGroup, scrollIntoViewScoped, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
7
|
+
import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, initFocusGroup, scrollIntoViewScoped, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
8
8
|
export { clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
9
|
-
import { signal, computed, effect, batch, untracked, useSignal } from "@preact/signals";
|
|
9
|
+
import { signal, computed, effect, batch, useComputed, untracked, useSignal } from "@preact/signals";
|
|
10
10
|
import { isValidElement, createContext, render, h, Fragment, toChildArray, options, cloneElement } from "preact";
|
|
11
11
|
import { useErrorBoundary, useLayoutEffect, useContext, useCallback, useRef, useState, useEffect, useMemo, useId } from "preact/hooks";
|
|
12
12
|
import { jsxs, jsx, Fragment as Fragment$1 } from "preact/jsx-runtime";
|
|
13
13
|
import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
|
|
14
|
-
import { parseDuration, durationContainsNaN, compareTwoDurations, durationToSeconds, createValidity, durationToISOString } from "@jsenv/validity";
|
|
14
|
+
import { parseDuration, durationContainsNaN, compareTwoDurations, durationToSeconds, DISPLAYABLE_RULE, MAX_LINE_BREAKS_RULE, NO_EMOJI_RULE, SINGLE_SPACE_RULE, createValidity, resolveCharClass, getCharClassMessageKey, compileCharClassAnchored, compileCharClass, CHAR_CLASS_PRESETS, durationToISOString } from "@jsenv/validity";
|
|
15
15
|
export { compareTwoDurations, durationContainsNaN, durationToHours, durationToISOString, durationToMinutes, durationToNumber, durationToSeconds, durationToString, parseDuration } from "@jsenv/validity";
|
|
16
16
|
import { Suspense, createPortal, forwardRef } from "preact/compat";
|
|
17
17
|
|
|
@@ -3676,8 +3676,55 @@ const findProxyControllers = (realInputId) => {
|
|
|
3676
3676
|
return proxyControllersByRealInputId.get(realInputId) ?? null;
|
|
3677
3677
|
};
|
|
3678
3678
|
|
|
3679
|
+
/**
|
|
3680
|
+
* The attributes constraints read, filled by each constraint module as it
|
|
3681
|
+
* evaluates. A constraint declares the attribute it wants (`"data-no-emoji"`)
|
|
3682
|
+
* and gets the prop for free: a control accepts the camelCase form
|
|
3683
|
+
* (`noEmoji`) and writes it on the control host under the attribute name — the
|
|
3684
|
+
* same conversion `element.dataset` does, so what a component is passed and
|
|
3685
|
+
* what ends up in the DOM read as one thing.
|
|
3686
|
+
*/
|
|
3687
|
+
|
|
3679
3688
|
const CONSTRAINT_ATTRIBUTE_SET = new Set();
|
|
3680
3689
|
|
|
3690
|
+
const dataAttributeCache = new Map();
|
|
3691
|
+
// A constraint imported lazily registers its attribute after controls have
|
|
3692
|
+
// already rendered, so an answer computed before it arrived must not survive it.
|
|
3693
|
+
let attributeCountWhenCached = 0;
|
|
3694
|
+
/**
|
|
3695
|
+
* The constraint attribute a prop stands for, `null` when it stands for none:
|
|
3696
|
+
* `"noEmoji"` → `"data-no-emoji"`.
|
|
3697
|
+
*/
|
|
3698
|
+
const constraintAttributeFromProp = (key) => {
|
|
3699
|
+
if (attributeCountWhenCached !== CONSTRAINT_ATTRIBUTE_SET.size) {
|
|
3700
|
+
dataAttributeCache.clear();
|
|
3701
|
+
attributeCountWhenCached = CONSTRAINT_ATTRIBUTE_SET.size;
|
|
3702
|
+
}
|
|
3703
|
+
const fromCache = dataAttributeCache.get(key);
|
|
3704
|
+
if (fromCache !== undefined) {
|
|
3705
|
+
return fromCache;
|
|
3706
|
+
}
|
|
3707
|
+
let attribute = null;
|
|
3708
|
+
// An attribute is already written as one (`data-no-emoji`, `aria-label`) —
|
|
3709
|
+
// there is nothing to convert, and the literal lookup has already happened.
|
|
3710
|
+
if (!key.includes("-")) {
|
|
3711
|
+
const candidate = `data-${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
|
|
3712
|
+
if (CONSTRAINT_ATTRIBUTE_SET.has(candidate)) {
|
|
3713
|
+
attribute = candidate;
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
dataAttributeCache.set(key, attribute);
|
|
3717
|
+
return attribute;
|
|
3718
|
+
};
|
|
3719
|
+
|
|
3720
|
+
/**
|
|
3721
|
+
* Whether a constraint attribute is on. Present means on — `""` is how HTML
|
|
3722
|
+
* writes a bare attribute — and only the values that say "passed, and off"
|
|
3723
|
+
* turn it off.
|
|
3724
|
+
*/
|
|
3725
|
+
const isConstraintAttributeOn = (value) =>
|
|
3726
|
+
value !== undefined && value !== null && value !== false;
|
|
3727
|
+
|
|
3681
3728
|
const CONSTRAINT_NAME_TO_PROP = {
|
|
3682
3729
|
disabled: "disabledMessage",
|
|
3683
3730
|
required: "requiredMessage",
|
|
@@ -3689,6 +3736,9 @@ const CONSTRAINT_NAME_TO_PROP = {
|
|
|
3689
3736
|
min: "minMessage",
|
|
3690
3737
|
max: "maxMessage",
|
|
3691
3738
|
single_space: "singleSpaceMessage",
|
|
3739
|
+
displayable: "displayableMessage",
|
|
3740
|
+
max_line_breaks: "maxLineBreaksMessage",
|
|
3741
|
+
no_emoji: "noEmojiMessage",
|
|
3692
3742
|
same_as: "sameAsMessage",
|
|
3693
3743
|
min_lower_letter: "minLowerLetterMessage",
|
|
3694
3744
|
min_upper_letter: "minUpperLetterMessage",
|
|
@@ -3755,10 +3805,13 @@ const getConstraintMessage = (
|
|
|
3755
3805
|
};
|
|
3756
3806
|
};
|
|
3757
3807
|
|
|
3758
|
-
// prop that we'll set on the control
|
|
3808
|
+
// prop that we'll set on the control.
|
|
3809
|
+
// CONSTRAINT_ATTRIBUTE_SET is consulted through controlAttributeFromProp()
|
|
3810
|
+
// rather than spread in here: a constraint registers into it when its own
|
|
3811
|
+
// module evaluates, so anything read at module-eval time reads a set that is
|
|
3812
|
+
// still filling up — and in a bundle, whichever constraint happens to evaluate
|
|
3813
|
+
// last would silently lose its attribute.
|
|
3759
3814
|
const CONTROL_ATTRIBUTE_SET = new Set([
|
|
3760
|
-
...CONSTRAINT_ATTRIBUTE_SET,
|
|
3761
|
-
|
|
3762
3815
|
"ref",
|
|
3763
3816
|
"children",
|
|
3764
3817
|
"id",
|
|
@@ -3800,7 +3853,6 @@ const CONTROL_ATTRIBUTE_SET = new Set([
|
|
|
3800
3853
|
]);
|
|
3801
3854
|
// prop concerning control but that won't end up in the DOM if not inside CONTROL_ATTRIBUTE_SET
|
|
3802
3855
|
const CONTROL_PROP_SET = new Set([
|
|
3803
|
-
...CONTROL_ATTRIBUTE_SET,
|
|
3804
3856
|
...CONSTRAINT_MESSAGE_PROP_NAME_SET,
|
|
3805
3857
|
|
|
3806
3858
|
"action",
|
|
@@ -3820,6 +3872,11 @@ const CONTROL_PROP_SET = new Set([
|
|
|
3820
3872
|
|
|
3821
3873
|
"loading",
|
|
3822
3874
|
"basePseudoState",
|
|
3875
|
+
// App constraints this control must satisfy, on top of the ones navi ships:
|
|
3876
|
+
// `constraints={[MY_CONSTRAINT]}`. A constraint is an object, so it carries
|
|
3877
|
+
// its own parameters — nothing has to travel through an attribute. Read on
|
|
3878
|
+
// every check in control_validation.js; `registerGlobalConstraint` is the
|
|
3879
|
+
// same thing for every control at once.
|
|
3823
3880
|
"constraints",
|
|
3824
3881
|
|
|
3825
3882
|
// A real target inside a zone that belongs to another control — see
|
|
@@ -3857,6 +3914,21 @@ const CONTROL_PROP_SET = new Set([
|
|
|
3857
3914
|
"maxLengthGuard",
|
|
3858
3915
|
]);
|
|
3859
3916
|
|
|
3917
|
+
/**
|
|
3918
|
+
* The attribute a prop must be written as on the control host, `null` when the
|
|
3919
|
+
* prop is not one. A constraint attribute may be passed either way — as the
|
|
3920
|
+
* attribute itself (`data-no-emoji`) or as the prop it stands for (`noEmoji`).
|
|
3921
|
+
*/
|
|
3922
|
+
const controlAttributeFromProp = (key) => {
|
|
3923
|
+
if (CONTROL_ATTRIBUTE_SET.has(key) || CONSTRAINT_ATTRIBUTE_SET.has(key)) {
|
|
3924
|
+
return key;
|
|
3925
|
+
}
|
|
3926
|
+
return constraintAttributeFromProp(key);
|
|
3927
|
+
};
|
|
3928
|
+
|
|
3929
|
+
const isControlProp = (key) =>
|
|
3930
|
+
CONTROL_PROP_SET.has(key) || controlAttributeFromProp(key) !== null;
|
|
3931
|
+
|
|
3860
3932
|
const MessagePropsRefContext = createContext();
|
|
3861
3933
|
|
|
3862
3934
|
const ControlIdContext = createContext();
|
|
@@ -7233,18 +7305,48 @@ naviI18n.addAll({
|
|
|
7233
7305
|
fr: "L'heure doit être <strong>[max]</strong> ou moins.",
|
|
7234
7306
|
en: "The time must be <strong>[max]</strong> or earlier.",
|
|
7235
7307
|
},
|
|
7236
|
-
"constraint.single_space.start
|
|
7308
|
+
"constraint.single_space.start": {
|
|
7237
7309
|
fr: "Ce champ ne doit pas commencer par un espace.",
|
|
7238
7310
|
en: "This field must not start with a space.",
|
|
7239
7311
|
},
|
|
7240
|
-
"constraint.single_space.end
|
|
7312
|
+
"constraint.single_space.end": {
|
|
7241
7313
|
fr: "Ce champ ne doit pas finir par un espace.",
|
|
7242
7314
|
en: "This field must not end with a space.",
|
|
7243
7315
|
},
|
|
7244
|
-
"constraint.single_space.consecutive
|
|
7316
|
+
"constraint.single_space.consecutive": {
|
|
7245
7317
|
fr: "Ce champ ne doit pas contenir plusieurs espaces consécutifs.",
|
|
7246
7318
|
en: "This field must not contain consecutive spaces.",
|
|
7247
7319
|
},
|
|
7320
|
+
// [sample] is the offending character with its marks — a stack is invisible
|
|
7321
|
+
// as a description and obvious as a sample.
|
|
7322
|
+
"constraint.displayable.stacked_marks.singular": {
|
|
7323
|
+
fr: "Ce champ contient un caractère qui empile plus de <strong>[max]</strong> signes : « [sample] ».",
|
|
7324
|
+
en: "This field contains a character stacking more than <strong>[max]</strong> marks: “[sample]”.",
|
|
7325
|
+
},
|
|
7326
|
+
"constraint.displayable.stacked_marks.plural": {
|
|
7327
|
+
fr: "Ce champ contient [count] caractères qui empilent plus de <strong>[max]</strong> signes (tel que « [sample] »).",
|
|
7328
|
+
en: "This field contains [count] characters stacking more than <strong>[max]</strong> marks (such as “[sample]”).",
|
|
7329
|
+
},
|
|
7330
|
+
"constraint.displayable.invisible": {
|
|
7331
|
+
fr: "Ce champ doit contenir au moins un caractère visible.",
|
|
7332
|
+
en: "This field must contain at least one visible character.",
|
|
7333
|
+
},
|
|
7334
|
+
"constraint.displayable.blank_lines": {
|
|
7335
|
+
fr: "Ce champ ne doit pas contenir plusieurs lignes vides consécutives.",
|
|
7336
|
+
en: "This field must not contain consecutive blank lines.",
|
|
7337
|
+
},
|
|
7338
|
+
"constraint.displayable.dangling_joiner": {
|
|
7339
|
+
fr: "Ce champ contient un caractère de liaison invisible qui ne relie rien.",
|
|
7340
|
+
en: "This field contains an invisible joiner that joins nothing.",
|
|
7341
|
+
},
|
|
7342
|
+
"constraint.no_emoji.default": {
|
|
7343
|
+
fr: "Ce champ ne doit pas contenir d'emoji.",
|
|
7344
|
+
en: "This field must not contain emoji.",
|
|
7345
|
+
},
|
|
7346
|
+
"constraint.max_line_breaks.default": {
|
|
7347
|
+
fr: "Ce champ ne doit pas contenir plus de [max] retour[s] à la ligne.",
|
|
7348
|
+
en: "This field must not contain more than [max] line break[s].",
|
|
7349
|
+
},
|
|
7248
7350
|
"constraint.min_lower_letter.password.singular": {
|
|
7249
7351
|
fr: "Ce mot de passe doit contenir au moins une lettre minuscule.",
|
|
7250
7352
|
en: "This password must contain at least one lowercase letter.",
|
|
@@ -7311,35 +7413,42 @@ naviI18n.addAll({
|
|
|
7311
7413
|
},
|
|
7312
7414
|
});
|
|
7313
7415
|
|
|
7314
|
-
//
|
|
7416
|
+
// Character class and maxLengthGuard messages. The char class keys are
|
|
7417
|
+
// @jsenv/validity's own ("char_class.slug"), prefixed with "constraint." —
|
|
7418
|
+
// the same sentence refuses a keystroke in a callout and a whole value in a
|
|
7419
|
+
// constraint, so there is one key for both.
|
|
7315
7420
|
naviI18n.addAll({
|
|
7316
7421
|
// Preset-specific char messages — more informative than the generic fallback
|
|
7317
|
-
"constraint.
|
|
7422
|
+
"constraint.char_class.numeric": {
|
|
7318
7423
|
fr: "Ce champ ne peut contenir que des chiffres.",
|
|
7319
7424
|
en: "This field can only contain digits.",
|
|
7320
7425
|
},
|
|
7321
|
-
"constraint.
|
|
7426
|
+
"constraint.char_class.alpha": {
|
|
7322
7427
|
fr: "Ce champ ne peut contenir que des lettres.",
|
|
7323
7428
|
en: "This field can only contain letters.",
|
|
7324
7429
|
},
|
|
7325
|
-
"constraint.
|
|
7430
|
+
"constraint.char_class.alphanumeric": {
|
|
7326
7431
|
fr: "Ce champ ne peut contenir que des lettres et des chiffres.",
|
|
7327
7432
|
en: "This field can only contain letters and digits.",
|
|
7328
7433
|
},
|
|
7329
|
-
"constraint.
|
|
7434
|
+
"constraint.char_class.uppercase": {
|
|
7330
7435
|
fr: "Ce champ ne peut contenir que des lettres majuscules.",
|
|
7331
7436
|
en: "This field can only contain uppercase letters.",
|
|
7332
7437
|
},
|
|
7333
|
-
"constraint.
|
|
7438
|
+
"constraint.char_class.hex": {
|
|
7334
7439
|
fr: "Ce champ ne peut contenir que des chiffres hexadécimaux (0-9, A-F).",
|
|
7335
7440
|
en: "This field can only contain hexadecimal digits (0-9, A-F).",
|
|
7336
7441
|
},
|
|
7337
|
-
"constraint.
|
|
7442
|
+
"constraint.char_class.slug": {
|
|
7338
7443
|
fr: "Ce champ ne peut contenir que des lettres minuscules, des chiffres et des tirets.",
|
|
7339
7444
|
en: "This field can only contain lowercase letters, digits, and hyphens.",
|
|
7340
7445
|
},
|
|
7341
7446
|
// Generic fallback for custom char classes and other presets (tel, card, postal, iban…)
|
|
7342
|
-
"constraint.
|
|
7447
|
+
"constraint.char_class.no_emoji": {
|
|
7448
|
+
fr: "Ce champ ne peut pas contenir d'emoji.",
|
|
7449
|
+
en: "This field cannot contain emoji.",
|
|
7450
|
+
},
|
|
7451
|
+
"constraint.char_class.default": {
|
|
7343
7452
|
fr: "Ce champ ne peut contenir que les caractères autorisés.",
|
|
7344
7453
|
en: "This field can only contain allowed characters.",
|
|
7345
7454
|
},
|
|
@@ -8487,7 +8596,11 @@ const PATTERN_CONSTRAINT = {
|
|
|
8487
8596
|
if (!valueAsString) {
|
|
8488
8597
|
return null;
|
|
8489
8598
|
}
|
|
8490
|
-
|
|
8599
|
+
// The `u` flag is how the platform compiles this same attribute, and what
|
|
8600
|
+
// lets a pattern speak about characters: `\p{...}` is only recognized under
|
|
8601
|
+
// it, and a range covers whole code points rather than the two halves an
|
|
8602
|
+
// astral character is made of.
|
|
8603
|
+
const regex = new RegExp(`^(?:${pattern})$`, "u");
|
|
8491
8604
|
if (regex.test(valueAsString)) {
|
|
8492
8605
|
return null;
|
|
8493
8606
|
}
|
|
@@ -10969,6 +11082,177 @@ const isFunctionButNotAnActionFunction = (action) => {
|
|
|
10969
11082
|
return typeof action === "function" && !action.isAction;
|
|
10970
11083
|
};
|
|
10971
11084
|
|
|
11085
|
+
/**
|
|
11086
|
+
* Where navi meets @jsenv/validity.
|
|
11087
|
+
*
|
|
11088
|
+
* validity names a refusal with a key and its parameters rather than a
|
|
11089
|
+
* sentence, so that a field and a server can refuse in the same words in the
|
|
11090
|
+
* language of the person reading. navi is one of those two callers: it says the
|
|
11091
|
+
* sentence, in the browser, through `naviI18n`. The keys line up on purpose —
|
|
11092
|
+
* validity's `"single_space.start"` is navi's `"constraint.single_space.start"`
|
|
11093
|
+
* — so overriding a message is looking up one key, not going through a
|
|
11094
|
+
* translation table.
|
|
11095
|
+
*
|
|
11096
|
+
* The other direction is `constraintFromValidityRule`: a rule an app wrote for
|
|
11097
|
+
* its server, worn by a control as a constraint.
|
|
11098
|
+
*/
|
|
11099
|
+
|
|
11100
|
+
|
|
11101
|
+
/**
|
|
11102
|
+
* Turns a @jsenv/validity rule into a constraint a control can wear, so an app
|
|
11103
|
+
* rule written once — in the package its server reads too — is checked on both
|
|
11104
|
+
* sides instead of being written twice.
|
|
11105
|
+
*
|
|
11106
|
+
* @param {object} rule
|
|
11107
|
+
* `{ name, applyOn(ruleValue, value, ruleConfig) }`, the same object passed
|
|
11108
|
+
* to `createValidity({ rules })`.
|
|
11109
|
+
* @param {object} [ruleConfig]
|
|
11110
|
+
* What parameterizes the rule, under its own name — `{ maxWords: 40 }` for a
|
|
11111
|
+
* rule named `maxWords`. Pass `formatMessage` here to say the refusal through
|
|
11112
|
+
* the app's own i18n; without it the key is looked up in `naviI18n` under
|
|
11113
|
+
* `constraint.<key>`, and a rule answering with a finished sentence is shown
|
|
11114
|
+
* as-is.
|
|
11115
|
+
*
|
|
11116
|
+
* Call it once, at module level: a constraint rebuilt on every render is a new
|
|
11117
|
+
* object on every check.
|
|
11118
|
+
*/
|
|
11119
|
+
const constraintFromValidityRule = (rule, ruleConfig = {}) => {
|
|
11120
|
+
const { formatMessage, ...ruleParams } = ruleConfig;
|
|
11121
|
+
return {
|
|
11122
|
+
name: rule.name,
|
|
11123
|
+
check: (field) => {
|
|
11124
|
+
const result = rule.applyOn(
|
|
11125
|
+
ruleParams[rule.name],
|
|
11126
|
+
field.uiState,
|
|
11127
|
+
ruleParams,
|
|
11128
|
+
);
|
|
11129
|
+
if (!result) {
|
|
11130
|
+
return null;
|
|
11131
|
+
}
|
|
11132
|
+
if (typeof result === "string") {
|
|
11133
|
+
return result;
|
|
11134
|
+
}
|
|
11135
|
+
if (formatMessage) {
|
|
11136
|
+
return formatMessage(result.key, result.params);
|
|
11137
|
+
}
|
|
11138
|
+
return naviI18nFromValidityMessage(result);
|
|
11139
|
+
},
|
|
11140
|
+
};
|
|
11141
|
+
};
|
|
11142
|
+
|
|
11143
|
+
const naviI18nFromValidityMessage = ({ key, params }) => {
|
|
11144
|
+
if (params && typeof params.max === "number") {
|
|
11145
|
+
// Lets a template pluralize on the bound it names: "[max] retour[s]".
|
|
11146
|
+
return naviI18n(`constraint.${key}`, {
|
|
11147
|
+
...params,
|
|
11148
|
+
s: params.max > 1 ? "s" : "",
|
|
11149
|
+
});
|
|
11150
|
+
}
|
|
11151
|
+
return naviI18n(`constraint.${key}`, params);
|
|
11152
|
+
};
|
|
11153
|
+
|
|
11154
|
+
/**
|
|
11155
|
+
* `data-displayable` — the value must be something the layout can actually
|
|
11156
|
+
* draw. What it refuses and why lives in @jsenv/validity's DISPLAYABLE_RULE:
|
|
11157
|
+
* these are display rules, not app rules, so a server re-checking the value
|
|
11158
|
+
* asks the exact same question and gets the exact same refusal.
|
|
11159
|
+
*
|
|
11160
|
+
* `data-max-stacked-marks` raises how many marks may stack on one base
|
|
11161
|
+
* character, for a language that needs more than the default.
|
|
11162
|
+
*/
|
|
11163
|
+
|
|
11164
|
+
|
|
11165
|
+
const DISPLAYABLE_CONSTRAINT = {
|
|
11166
|
+
name: "displayable",
|
|
11167
|
+
messageAttribute: "data-displayable-message",
|
|
11168
|
+
check: (field) => {
|
|
11169
|
+
const displayable = field.controlHostProps["data-displayable"];
|
|
11170
|
+
if (!isConstraintAttributeOn(displayable)) {
|
|
11171
|
+
return null;
|
|
11172
|
+
}
|
|
11173
|
+
const valueAsString =
|
|
11174
|
+
field.uiState === undefined ? "" : String(field.uiState);
|
|
11175
|
+
const maxStackedMarksAttribute =
|
|
11176
|
+
field.controlHostProps["data-max-stacked-marks"];
|
|
11177
|
+
const result = DISPLAYABLE_RULE.applyOn(true, valueAsString, {
|
|
11178
|
+
maxStackedMarks:
|
|
11179
|
+
maxStackedMarksAttribute === undefined
|
|
11180
|
+
? undefined
|
|
11181
|
+
: parseInt(maxStackedMarksAttribute, 10),
|
|
11182
|
+
});
|
|
11183
|
+
if (!result) {
|
|
11184
|
+
return null;
|
|
11185
|
+
}
|
|
11186
|
+
return naviI18nFromValidityMessage(result);
|
|
11187
|
+
},
|
|
11188
|
+
};
|
|
11189
|
+
CONSTRAINT_ATTRIBUTE_SET.add("data-displayable");
|
|
11190
|
+
CONSTRAINT_ATTRIBUTE_SET.add("data-max-stacked-marks");
|
|
11191
|
+
|
|
11192
|
+
/**
|
|
11193
|
+
* `data-max-line-breaks` — how many line breaks the value may hold. Counted in
|
|
11194
|
+
* breaks rather than in lines because how many lines a value renders as depends
|
|
11195
|
+
* on wrapping, which is the layout's answer, not the value's.
|
|
11196
|
+
*
|
|
11197
|
+
* The rule is @jsenv/validity's MAX_LINE_BREAKS_RULE — a textarea in the
|
|
11198
|
+
* browser and a server receiving the value both ask it.
|
|
11199
|
+
*/
|
|
11200
|
+
|
|
11201
|
+
|
|
11202
|
+
const MAX_LINE_BREAKS_CONSTRAINT = {
|
|
11203
|
+
name: "max_line_breaks",
|
|
11204
|
+
messageAttribute: "data-max-line-breaks-message",
|
|
11205
|
+
check: (field) => {
|
|
11206
|
+
const maxLineBreaksAttribute =
|
|
11207
|
+
field.controlHostProps["data-max-line-breaks"];
|
|
11208
|
+
if (!isConstraintAttributeOn(maxLineBreaksAttribute)) {
|
|
11209
|
+
return null;
|
|
11210
|
+
}
|
|
11211
|
+
const maxLineBreaks = parseInt(maxLineBreaksAttribute, 10);
|
|
11212
|
+
if (isNaN(maxLineBreaks)) {
|
|
11213
|
+
return null;
|
|
11214
|
+
}
|
|
11215
|
+
const valueAsString =
|
|
11216
|
+
field.uiState === undefined ? "" : String(field.uiState);
|
|
11217
|
+
const result = MAX_LINE_BREAKS_RULE.applyOn(maxLineBreaks, valueAsString);
|
|
11218
|
+
if (!result) {
|
|
11219
|
+
return null;
|
|
11220
|
+
}
|
|
11221
|
+
return naviI18nFromValidityMessage(result);
|
|
11222
|
+
},
|
|
11223
|
+
};
|
|
11224
|
+
CONSTRAINT_ATTRIBUTE_SET.add("data-max-line-breaks");
|
|
11225
|
+
|
|
11226
|
+
/**
|
|
11227
|
+
* `data-no-emoji` — an app is free with emoji or it is not, and that is not the
|
|
11228
|
+
* layout's call: a row survives an emoji, a legal name, an identifier or a
|
|
11229
|
+
* title may still not want one. So it is its own switch rather than a part of
|
|
11230
|
+
* `data-displayable`.
|
|
11231
|
+
*
|
|
11232
|
+
* The rule is @jsenv/validity's NO_EMOJI_RULE. To refuse the keystroke instead
|
|
11233
|
+
* of the value, `charGuard="noEmoji"` is the same knowledge on the other side.
|
|
11234
|
+
*/
|
|
11235
|
+
|
|
11236
|
+
|
|
11237
|
+
const NO_EMOJI_CONSTRAINT = {
|
|
11238
|
+
name: "no_emoji",
|
|
11239
|
+
messageAttribute: "data-no-emoji-message",
|
|
11240
|
+
check: (field) => {
|
|
11241
|
+
const noEmoji = field.controlHostProps["data-no-emoji"];
|
|
11242
|
+
if (!isConstraintAttributeOn(noEmoji)) {
|
|
11243
|
+
return null;
|
|
11244
|
+
}
|
|
11245
|
+
const valueAsString =
|
|
11246
|
+
field.uiState === undefined ? "" : String(field.uiState);
|
|
11247
|
+
const result = NO_EMOJI_RULE.applyOn(true, valueAsString);
|
|
11248
|
+
if (!result) {
|
|
11249
|
+
return null;
|
|
11250
|
+
}
|
|
11251
|
+
return naviI18nFromValidityMessage(result);
|
|
11252
|
+
},
|
|
11253
|
+
};
|
|
11254
|
+
CONSTRAINT_ATTRIBUTE_SET.add("data-no-emoji");
|
|
11255
|
+
|
|
10972
11256
|
const MIN_LOWER_LETTER_CONSTRAINT = {
|
|
10973
11257
|
name: "min_lower_letter",
|
|
10974
11258
|
messageAttribute: "data-min-lower-letter-message",
|
|
@@ -11238,30 +11522,28 @@ const SAME_AS_CONSTRAINT = {
|
|
|
11238
11522
|
};
|
|
11239
11523
|
CONSTRAINT_ATTRIBUTE_SET.add("data-same-as");
|
|
11240
11524
|
|
|
11525
|
+
/**
|
|
11526
|
+
* `data-single-space` — no leading or trailing space, never two in a row.
|
|
11527
|
+
* The rule itself is @jsenv/validity's SINGLE_SPACE_RULE, so a server checking
|
|
11528
|
+
* the value again refuses it for the same reason and in the same words.
|
|
11529
|
+
*/
|
|
11530
|
+
|
|
11531
|
+
|
|
11241
11532
|
const SINGLE_SPACE_CONSTRAINT = {
|
|
11242
11533
|
name: "single_space",
|
|
11243
11534
|
messageAttribute: "data-single-space-message",
|
|
11244
11535
|
check: (field) => {
|
|
11245
11536
|
const singleSpace = field.controlHostProps["data-single-space"];
|
|
11246
|
-
if (singleSpace
|
|
11537
|
+
if (!isConstraintAttributeOn(singleSpace)) {
|
|
11247
11538
|
return null;
|
|
11248
11539
|
}
|
|
11249
|
-
|
|
11250
11540
|
const valueAsString =
|
|
11251
11541
|
field.uiState === undefined ? "" : String(field.uiState);
|
|
11252
|
-
const
|
|
11253
|
-
|
|
11254
|
-
const hasDoubleSpace = valueAsString.includes(" ");
|
|
11255
|
-
if (!hasLeadingSpace && !hasTrailingSpace && !hasDoubleSpace) {
|
|
11542
|
+
const result = SINGLE_SPACE_RULE.applyOn(true, valueAsString);
|
|
11543
|
+
if (!result) {
|
|
11256
11544
|
return null;
|
|
11257
11545
|
}
|
|
11258
|
-
|
|
11259
|
-
return naviI18n("constraint.single_space.start.default");
|
|
11260
|
-
}
|
|
11261
|
-
if (hasTrailingSpace) {
|
|
11262
|
-
return naviI18n("constraint.single_space.end.default");
|
|
11263
|
-
}
|
|
11264
|
-
return naviI18n("constraint.single_space.consecutive.default");
|
|
11546
|
+
return naviI18nFromValidityMessage(result);
|
|
11265
11547
|
},
|
|
11266
11548
|
};
|
|
11267
11549
|
CONSTRAINT_ATTRIBUTE_SET.add("data-single-space");
|
|
@@ -11386,6 +11668,9 @@ const STANDARD_CONSTRAINT_SET = new Set([
|
|
|
11386
11668
|
const NAVI_CONSTRAINT_SET = new Set([
|
|
11387
11669
|
MIN_SPECIAL_CHAR_CONSTRAINT,
|
|
11388
11670
|
SINGLE_SPACE_CONSTRAINT,
|
|
11671
|
+
DISPLAYABLE_CONSTRAINT,
|
|
11672
|
+
MAX_LINE_BREAKS_CONSTRAINT,
|
|
11673
|
+
NO_EMOJI_CONSTRAINT,
|
|
11389
11674
|
MIN_DIGIT_CONSTRAINT,
|
|
11390
11675
|
MIN_UPPER_LETTER_CONSTRAINT,
|
|
11391
11676
|
MIN_LOWER_LETTER_CONSTRAINT,
|
|
@@ -11398,8 +11683,29 @@ const DEFAULT_CONSTRAINT_SET = new Set([
|
|
|
11398
11683
|
...NAVI_CONSTRAINT_SET,
|
|
11399
11684
|
]);
|
|
11400
11685
|
const registerGlobalConstraint = (customConstraint) => {
|
|
11401
|
-
|
|
11402
|
-
|
|
11686
|
+
const constraint = normalizeConstraint(customConstraint);
|
|
11687
|
+
NAVI_CONSTRAINT_SET.add(constraint);
|
|
11688
|
+
DEFAULT_CONSTRAINT_SET.add(constraint);
|
|
11689
|
+
};
|
|
11690
|
+
|
|
11691
|
+
// A constraint may be written as a bare check function; the rest of the code
|
|
11692
|
+
// wants the object shape. The wrapper is cached so a function passed on every
|
|
11693
|
+
// render keeps one identity across checks.
|
|
11694
|
+
const constraintFromFunctionMap = new WeakMap();
|
|
11695
|
+
const normalizeConstraint = (constraint) => {
|
|
11696
|
+
if (typeof constraint !== "function") {
|
|
11697
|
+
return constraint;
|
|
11698
|
+
}
|
|
11699
|
+
const existing = constraintFromFunctionMap.get(constraint);
|
|
11700
|
+
if (existing) {
|
|
11701
|
+
return existing;
|
|
11702
|
+
}
|
|
11703
|
+
const constraintObject = {
|
|
11704
|
+
name: constraint.name || "custom_function",
|
|
11705
|
+
check: constraint,
|
|
11706
|
+
};
|
|
11707
|
+
constraintFromFunctionMap.set(constraint, constraintObject);
|
|
11708
|
+
return constraintObject;
|
|
11403
11709
|
};
|
|
11404
11710
|
|
|
11405
11711
|
const createControlValidation = (
|
|
@@ -11415,15 +11721,10 @@ const createControlValidation = (
|
|
|
11415
11721
|
const dynamicConstraintSet = new Set();
|
|
11416
11722
|
{
|
|
11417
11723
|
controlValidity.registerConstraint = (constraint) => {
|
|
11418
|
-
|
|
11419
|
-
|
|
11420
|
-
name: constraint.name || "custom_function",
|
|
11421
|
-
check: constraint,
|
|
11422
|
-
};
|
|
11423
|
-
}
|
|
11424
|
-
dynamicConstraintSet.add(constraint);
|
|
11724
|
+
const constraintObject = normalizeConstraint(constraint);
|
|
11725
|
+
dynamicConstraintSet.add(constraintObject);
|
|
11425
11726
|
return () => {
|
|
11426
|
-
dynamicConstraintSet.delete(
|
|
11727
|
+
dynamicConstraintSet.delete(constraintObject);
|
|
11427
11728
|
};
|
|
11428
11729
|
};
|
|
11429
11730
|
}
|
|
@@ -11480,6 +11781,16 @@ const createControlValidation = (
|
|
|
11480
11781
|
...DEFAULT_CONSTRAINT_SET,
|
|
11481
11782
|
...dynamicConstraintSet,
|
|
11482
11783
|
]);
|
|
11784
|
+
// An app constraint declared at the call site: `constraints={[MY_CONSTRAINT]}`.
|
|
11785
|
+
// Read from the raw props on every check so a constraint whose parameters
|
|
11786
|
+
// are closed over is re-created freely, and last so the constraints navi
|
|
11787
|
+
// ships are the ones reported first (see pickConstraintFailureInfo).
|
|
11788
|
+
const constraintsFromProps = controller.props.constraints;
|
|
11789
|
+
if (constraintsFromProps) {
|
|
11790
|
+
for (const constraintFromProps of constraintsFromProps) {
|
|
11791
|
+
constraintSet.add(normalizeConstraint(constraintFromProps));
|
|
11792
|
+
}
|
|
11793
|
+
}
|
|
11483
11794
|
const elementSig = getElementSignature(controller.ref.current);
|
|
11484
11795
|
// Not logged: every control checks its constraints on every interaction and
|
|
11485
11796
|
// almost always passes, so this line alone was most of the debug output —
|
|
@@ -11531,6 +11842,13 @@ const createControlValidation = (
|
|
|
11531
11842
|
}
|
|
11532
11843
|
}
|
|
11533
11844
|
|
|
11845
|
+
// Several constraints can fail at once and only one sentence is shown —
|
|
11846
|
+
// naming it here lets whoever draws its own summary of the failures say the
|
|
11847
|
+
// same thing as the callout instead of picking a second one.
|
|
11848
|
+
newConstraintValidityState.reported = failedConstraintInfo
|
|
11849
|
+
? failedConstraintInfo.name
|
|
11850
|
+
: null;
|
|
11851
|
+
|
|
11534
11852
|
const activeFailedConstraintInfo = failedConstraintInfo;
|
|
11535
11853
|
if (activeFailedConstraintInfo) {
|
|
11536
11854
|
const titleLess = controller.controlHostProps.title === undefined;
|
|
@@ -12807,12 +13125,18 @@ const useArraySignalMembership = (...args) => {
|
|
|
12807
13125
|
"useArraySignalMembership requires at least 2 arguments: [arraySignal, id]",
|
|
12808
13126
|
);
|
|
12809
13127
|
}
|
|
13128
|
+
const [arraySignal, id] = args;
|
|
12810
13129
|
|
|
12811
|
-
|
|
12812
|
-
|
|
12813
|
-
|
|
12814
|
-
|
|
12815
|
-
|
|
13130
|
+
// Through a computed so the component re-renders when its own membership
|
|
13131
|
+
// changes, not every time anything else is added to or removed from the
|
|
13132
|
+
// array: a list of 200 rows each watching the same array would otherwise all
|
|
13133
|
+
// re-render (and each re-scan the array) when one row is toggled.
|
|
13134
|
+
const isMember = useComputed(() => arraySignal.value.includes(id)).value;
|
|
13135
|
+
const [, add, remove] = useMemo(
|
|
13136
|
+
() => arraySignalMembership(arraySignal, id),
|
|
13137
|
+
[arraySignal, id],
|
|
13138
|
+
);
|
|
13139
|
+
return [isMember, add, remove];
|
|
12816
13140
|
};
|
|
12817
13141
|
|
|
12818
13142
|
const arraySignalMembership = (...args) => {
|
|
@@ -14169,7 +14493,7 @@ const createRangeReader = (
|
|
|
14169
14493
|
// dropped here is one that gets asked for again after a remount.
|
|
14170
14494
|
readRange.trimComposition = (keepFrom, keepTo, budget) => {
|
|
14171
14495
|
const composition = findComposition(currentParams());
|
|
14172
|
-
if (!composition ||
|
|
14496
|
+
if (!composition || composition.idByIndex.size <= budget) {
|
|
14173
14497
|
return;
|
|
14174
14498
|
}
|
|
14175
14499
|
for (const index of composition.idByIndex.keys()) {
|
|
@@ -19726,6 +20050,21 @@ import.meta.css = [/* css */`
|
|
|
19726
20050
|
}
|
|
19727
20051
|
}
|
|
19728
20052
|
|
|
20053
|
+
/* A corner claim travels down (see group.jsx) because the member joined to
|
|
20054
|
+
its neighbours is not always the thing that draws the frame: it can be a
|
|
20055
|
+
bare wrapper, a tooltip, a link, and the control inside is what has to
|
|
20056
|
+
square. That only holds while the wrapper adds nothing of its own. A box
|
|
20057
|
+
that paints a background or a border, or that insets what it holds, IS the
|
|
20058
|
+
frame at that spot — what is inside it sits on padding or on that
|
|
20059
|
+
background, never on the corner the group squared — so the claim stops
|
|
20060
|
+
here, exactly as a control stops it once it has answered. */
|
|
20061
|
+
[navi-box-frame] > * {
|
|
20062
|
+
--x-corner-top-left-radius: initial;
|
|
20063
|
+
--x-corner-top-right-radius: initial;
|
|
20064
|
+
--x-corner-bottom-right-radius: initial;
|
|
20065
|
+
--x-corner-bottom-left-radius: initial;
|
|
20066
|
+
}
|
|
20067
|
+
|
|
19729
20068
|
@layer navi {
|
|
19730
20069
|
/*
|
|
19731
20070
|
When using square/circle/aspectRatio prop we expect box to respect the aspect ratio.
|
|
@@ -20012,10 +20351,27 @@ const computeBox = (props, parentBoxFlow) => {
|
|
|
20012
20351
|
rest["data-body"] = "";
|
|
20013
20352
|
// Padding is what decides whether the content reaches the body's own
|
|
20014
20353
|
// corners — see the corner claims in this file's CSS.
|
|
20015
|
-
|
|
20354
|
+
let flush = true;
|
|
20355
|
+
for (const name of PADDING_PROP_SET) {
|
|
20356
|
+
if (declaresSomething(rest[name])) {
|
|
20357
|
+
flush = false;
|
|
20358
|
+
break;
|
|
20359
|
+
}
|
|
20360
|
+
}
|
|
20361
|
+
if (flush) {
|
|
20016
20362
|
rest["data-body-flush"] = "";
|
|
20017
20363
|
}
|
|
20018
20364
|
}
|
|
20365
|
+
// A box that paints something of its own, or that insets what it holds, is
|
|
20366
|
+
// the frame at that spot: the corner claims coming from a Group are about
|
|
20367
|
+
// ITS corners and nothing inside reaches them, so they stop here — see this
|
|
20368
|
+
// file's CSS.
|
|
20369
|
+
for (const name of FRAME_PROP_SET) {
|
|
20370
|
+
if (declaresSomething(rest[name])) {
|
|
20371
|
+
rest["navi-box-frame"] = "";
|
|
20372
|
+
break;
|
|
20373
|
+
}
|
|
20374
|
+
}
|
|
20019
20375
|
const defaultDisplay = getDefaultDisplay(TagName);
|
|
20020
20376
|
let {
|
|
20021
20377
|
inline,
|
|
@@ -20446,8 +20802,14 @@ const shouldInjectSeparatorBetween = (left, right) => {
|
|
|
20446
20802
|
}
|
|
20447
20803
|
return true;
|
|
20448
20804
|
};
|
|
20449
|
-
const
|
|
20450
|
-
|
|
20805
|
+
const PADDING_PROP_SET = new Set(["padding", "paddingX", "paddingY", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft"]);
|
|
20806
|
+
/* What makes a box the frame at its spot: what it paints of its own, and the
|
|
20807
|
+
padding holding its content away from its corners. Deliberately without the
|
|
20808
|
+
radius props: a radius alone paints nothing — it only says how a background
|
|
20809
|
+
or a border already there is cut — so a box carrying just a radius is still
|
|
20810
|
+
a wrapper around whatever draws. */
|
|
20811
|
+
const FRAME_PROP_SET = new Set(["background", "backgroundColor", "backgroundImage", "border", "borderTop", "borderRight", "borderBottom", "borderLeft", "borderWidth", "borderColor", "borderStyle", ...PADDING_PROP_SET]);
|
|
20812
|
+
const declaresSomething = value => {
|
|
20451
20813
|
if (value === undefined || value === null || value === false) {
|
|
20452
20814
|
return false;
|
|
20453
20815
|
}
|
|
@@ -30042,6 +30404,13 @@ registerNaviCommand("--navi-unselect", (source, event) => {
|
|
|
30042
30404
|
* observeAncestorOpenState for exactly how that's detected, and why it
|
|
30043
30405
|
* matters that it happens before the browser paints.
|
|
30044
30406
|
* - Inside an open ancestor → runs on mount AND every subsequent open.
|
|
30407
|
+
* - Inside the always-on-screen part of a *closed* one — a picker's façade,
|
|
30408
|
+
* an expandable's header, a <summary>: those elements are displayed the
|
|
30409
|
+
* whole time their ancestor reads as closed (aria-expanded on a trigger
|
|
30410
|
+
* describes the popup it controls, not its own contents). They run on
|
|
30411
|
+
* mount like anything else on screen, and the ancestor opening later
|
|
30412
|
+
* reveals nothing about them, so it does not re-run them either. See
|
|
30413
|
+
* isDisplayedDespiteClosedAncestor in @jsenv/dom.
|
|
30045
30414
|
*
|
|
30046
30415
|
* The callback's second argument is always a `navi_displayed` CustomEvent,
|
|
30047
30416
|
* with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
|
|
@@ -30074,6 +30443,10 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
30074
30443
|
const callbackRef = useRef(callback);
|
|
30075
30444
|
callbackRef.current = callback;
|
|
30076
30445
|
|
|
30446
|
+
// Set by the mount effect below for an element that lives in its openable
|
|
30447
|
+
// ancestor's façade rather than in what that ancestor opens.
|
|
30448
|
+
const displayedWhileAncestorClosedRef = useRef(false);
|
|
30449
|
+
|
|
30077
30450
|
// Run on mount (or when deps change) — but only if the element is visible.
|
|
30078
30451
|
useLayoutEffect(() => {
|
|
30079
30452
|
const el = ref.current;
|
|
@@ -30086,9 +30459,16 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
30086
30459
|
return;
|
|
30087
30460
|
}
|
|
30088
30461
|
if (!isAncestorOpen(ancestor)) {
|
|
30089
|
-
|
|
30090
|
-
|
|
30091
|
-
|
|
30462
|
+
if (!isDisplayedDespiteClosedAncestor(el)) {
|
|
30463
|
+
// Ancestor is closed and took this element off screen with it — skip
|
|
30464
|
+
// now; the observeAncestorOpenState call below will fire once it
|
|
30465
|
+
// opens.
|
|
30466
|
+
return;
|
|
30467
|
+
}
|
|
30468
|
+
// Closed, yet on screen: the ancestor is the trigger of what is
|
|
30469
|
+
// closed, not the thing itself, and this element belongs to the façade
|
|
30470
|
+
// it keeps showing.
|
|
30471
|
+
displayedWhileAncestorClosedRef.current = true;
|
|
30092
30472
|
}
|
|
30093
30473
|
callbackRef.current(el, createDisplayedEvent(ancestor, false));
|
|
30094
30474
|
}, deps);
|
|
@@ -30107,6 +30487,12 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
30107
30487
|
if (!isOpen) {
|
|
30108
30488
|
return;
|
|
30109
30489
|
}
|
|
30490
|
+
if (displayedWhileAncestorClosedRef.current) {
|
|
30491
|
+
// Façade content: on screen the whole time, so this opening reveals
|
|
30492
|
+
// nothing here — and `becauseAncestorOpened: true` about it would be
|
|
30493
|
+
// false in a way consumers act on (see use_auto_focus.js).
|
|
30494
|
+
return;
|
|
30495
|
+
}
|
|
30110
30496
|
const lastEl = ref.current;
|
|
30111
30497
|
callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
|
|
30112
30498
|
});
|
|
@@ -30896,59 +31282,20 @@ const subscribeToControlState = (controlId, callback) => {
|
|
|
30896
31282
|
|
|
30897
31283
|
const FormContext = createContext();
|
|
30898
31284
|
|
|
30899
|
-
/**
|
|
30900
|
-
* Named presets for the `charGuard` prop.
|
|
30901
|
-
* Each value is a regex character class (including the [ ] delimiters).
|
|
30902
|
-
*/
|
|
30903
|
-
const CHAR_CLASS_PRESETS = {
|
|
30904
|
-
numeric: "[0-9]", // digits only
|
|
30905
|
-
alpha: "[A-Za-z]", // letters only
|
|
30906
|
-
alphanumeric: "[0-9A-Za-z]", // letters and digits
|
|
30907
|
-
decimal: "[-0-9.,]", // digits, minus, dot, comma
|
|
30908
|
-
uppercase: "[A-Z]", // uppercase letters only
|
|
30909
|
-
tel: "[-0-9+() ]", // phone: digits, +, -, parens, space
|
|
30910
|
-
email: "[a-zA-Z0-9._%+@-]", // email characters
|
|
30911
|
-
card: "[0-9 ]", // credit card: digits and spaces
|
|
30912
|
-
hex: "[0-9A-Fa-f]", // hexadecimal digits
|
|
30913
|
-
pin: "[0-9]", // numeric PIN
|
|
30914
|
-
postal: "[0-9A-Za-z -]", // postal code (FR, UK, US)
|
|
30915
|
-
iban: "[0-9A-Z]", // IBAN: uppercase and digits
|
|
30916
|
-
slug: "[a-z0-9-]", // URL slug
|
|
30917
|
-
};
|
|
30918
|
-
|
|
30919
|
-
// Specific i18n keys per preset — more informative than the generic fallback
|
|
30920
|
-
const MESSAGE_KEY_FROM_PRESET = {
|
|
30921
|
-
numeric: "constraint.guard.number",
|
|
30922
|
-
pin: "constraint.guard.number",
|
|
30923
|
-
alpha: "constraint.guard.alpha",
|
|
30924
|
-
alphanumeric: "constraint.guard.alphanumeric",
|
|
30925
|
-
uppercase: "constraint.guard.uppercase",
|
|
30926
|
-
hex: "constraint.guard.hex",
|
|
30927
|
-
slug: "constraint.guard.slug",
|
|
30928
|
-
// tel, card, postal, iban, custom → generic fallback
|
|
30929
|
-
};
|
|
30930
|
-
|
|
30931
|
-
/**
|
|
30932
|
-
* Returns the regex character class for a preset name, or the raw value as-is
|
|
30933
|
-
* if it's not a known preset (allows custom classes like "[A-Z0-9_]").
|
|
30934
|
-
*/
|
|
30935
|
-
const resolveCharClass = (value) => {
|
|
30936
|
-
if (!value) return null;
|
|
30937
|
-
return CHAR_CLASS_PRESETS[value] ?? value;
|
|
30938
|
-
};
|
|
30939
|
-
|
|
30940
|
-
/**
|
|
30941
|
-
* Returns the i18n key for the char guard rejection message.
|
|
30942
|
-
* Falls back to the generic "constraint.guard.chars" for custom classes and
|
|
30943
|
-
* presets without a specific message.
|
|
30944
|
-
*/
|
|
30945
|
-
const getCharGuardMessageKey = (value) => {
|
|
30946
|
-
return MESSAGE_KEY_FROM_PRESET[value] ?? "constraint.guard.chars";
|
|
30947
|
-
};
|
|
30948
|
-
|
|
30949
31285
|
/**
|
|
30950
31286
|
* Input guard — enforces character and length constraints during typing, paste,
|
|
30951
|
-
* and external value sets.
|
|
31287
|
+
* and external value sets. What a character class holds and which sentence
|
|
31288
|
+
* refuses it comes from @jsenv/validity; the guard is what only a field can do,
|
|
31289
|
+
* blocking the keystroke before the value exists.
|
|
31290
|
+
*
|
|
31291
|
+
* The guard answers for the GESTURE, never for what the control already holds.
|
|
31292
|
+
* A value can arrive already outside the class or already too long — a
|
|
31293
|
+
* `defaultValue`, a signal, a value written from elsewhere — and refusing the
|
|
31294
|
+
* next keystroke over it would blame the person for a character they did not
|
|
31295
|
+
* type. So a change is refused only when it makes the value worse: one more
|
|
31296
|
+
* character outside the class, or one more character over the limit. What is
|
|
31297
|
+
* already there is the `charClass`/`maxLength` constraint's business, and it
|
|
31298
|
+
* says so at submit.
|
|
30952
31299
|
*
|
|
30953
31300
|
* The guard owns a single callout token (shared across all rejection reasons) so
|
|
30954
31301
|
* successive rejections update the same callout rather than stacking.
|
|
@@ -30969,9 +31316,13 @@ const s = (n) => (n > 1 ? "s" : "");
|
|
|
30969
31316
|
// Keydown: block only single printable characters that don't match the class.
|
|
30970
31317
|
// Multi-character key names (Delete, ArrowLeft…) are always allowed.
|
|
30971
31318
|
const getInvalidCharMessage = (char, { charClass, messageKey }) => {
|
|
30972
|
-
|
|
30973
|
-
|
|
30974
|
-
|
|
31319
|
+
// Counted in code points: an astral character is one character typed, not two.
|
|
31320
|
+
const codePointCount = [...char].length;
|
|
31321
|
+
if (codePointCount !== 1) {
|
|
31322
|
+
return null;
|
|
31323
|
+
}
|
|
31324
|
+
if (compileCharClass(charClass).test(char)) return null;
|
|
31325
|
+
return naviI18nFromValidityMessage({ key: messageKey });
|
|
30975
31326
|
};
|
|
30976
31327
|
|
|
30977
31328
|
// Keydown: block when inserting one char would exceed maxLength.
|
|
@@ -30987,18 +31338,41 @@ const getMaxLengthInsertionMessage = (el, { maxLength }) => {
|
|
|
30987
31338
|
});
|
|
30988
31339
|
};
|
|
30989
31340
|
|
|
30990
|
-
|
|
30991
|
-
const
|
|
31341
|
+
const countCharsOutsideClass = (str, charClass) => {
|
|
31342
|
+
const regex = compileCharClass(charClass);
|
|
31343
|
+
let count = 0;
|
|
31344
|
+
for (const char of str) {
|
|
31345
|
+
if (!regex.test(char)) {
|
|
31346
|
+
count++;
|
|
31347
|
+
}
|
|
31348
|
+
}
|
|
31349
|
+
return count;
|
|
31350
|
+
};
|
|
31351
|
+
|
|
31352
|
+
// Paste / set: block when the gesture brings in a character the class refuses.
|
|
31353
|
+
const getInvalidCharsMessage = (
|
|
31354
|
+
uiState,
|
|
31355
|
+
{ charClass, messageKey, uiStateNow },
|
|
31356
|
+
) => {
|
|
30992
31357
|
const str = uiState === undefined ? "" : String(uiState);
|
|
30993
|
-
if (
|
|
30994
|
-
|
|
31358
|
+
if (compileCharClassAnchored(charClass).test(str)) return null;
|
|
31359
|
+
const strNow = uiStateNow === undefined ? "" : String(uiStateNow);
|
|
31360
|
+
if (
|
|
31361
|
+
countCharsOutsideClass(str, charClass) <=
|
|
31362
|
+
countCharsOutsideClass(strNow, charClass)
|
|
31363
|
+
) {
|
|
31364
|
+
return null;
|
|
31365
|
+
}
|
|
31366
|
+
return naviI18nFromValidityMessage({ key: messageKey });
|
|
30995
31367
|
};
|
|
30996
31368
|
|
|
30997
|
-
// Paste / set: truncate
|
|
30998
|
-
const getLengthOverflowResult = (uiState, { maxLength }) => {
|
|
31369
|
+
// Paste / set: truncate what the gesture adds beyond the limit.
|
|
31370
|
+
const getLengthOverflowResult = (uiState, { maxLength, uiStateNow }) => {
|
|
30999
31371
|
if (maxLength === undefined) return null;
|
|
31000
31372
|
const str = uiState === undefined ? "" : String(uiState);
|
|
31001
31373
|
if (str.length <= maxLength) return null;
|
|
31374
|
+
const strNow = uiStateNow === undefined ? "" : String(uiStateNow);
|
|
31375
|
+
if (str.length <= strNow.length) return null;
|
|
31002
31376
|
return {
|
|
31003
31377
|
fixedValue: str.slice(0, maxLength),
|
|
31004
31378
|
message: naviI18n("constraint.guard.max_length.value", {
|
|
@@ -31037,7 +31411,7 @@ const createControlGuard = (controller) => {
|
|
|
31037
31411
|
|
|
31038
31412
|
if (charGuard) {
|
|
31039
31413
|
const charClass = resolveCharClass(charGuard);
|
|
31040
|
-
const messageKey =
|
|
31414
|
+
const messageKey = getCharClassMessageKey(charGuard);
|
|
31041
31415
|
const charMsg = getInvalidCharMessage(e.key, { charClass, messageKey });
|
|
31042
31416
|
if (charMsg) {
|
|
31043
31417
|
show(charMsg, e);
|
|
@@ -31067,13 +31441,17 @@ const createControlGuard = (controller) => {
|
|
|
31067
31441
|
*/
|
|
31068
31442
|
const checkUIState = (uiState, e) => {
|
|
31069
31443
|
const { charGuard, maxLengthGuard } = controller.props;
|
|
31444
|
+
// What the control holds right now — setUIState has not written the new
|
|
31445
|
+
// value yet, and the paste path computes it without applying it.
|
|
31446
|
+
const uiStateNow = controller.uiState;
|
|
31070
31447
|
|
|
31071
31448
|
if (charGuard) {
|
|
31072
31449
|
const charClass = resolveCharClass(charGuard);
|
|
31073
|
-
const messageKey =
|
|
31450
|
+
const messageKey = getCharClassMessageKey(charGuard);
|
|
31074
31451
|
const charsMsg = getInvalidCharsMessage(uiState, {
|
|
31075
31452
|
charClass,
|
|
31076
31453
|
messageKey,
|
|
31454
|
+
uiStateNow,
|
|
31077
31455
|
});
|
|
31078
31456
|
if (charsMsg) {
|
|
31079
31457
|
show(charsMsg, e);
|
|
@@ -31084,6 +31462,7 @@ const createControlGuard = (controller) => {
|
|
|
31084
31462
|
if (maxLengthGuard !== undefined) {
|
|
31085
31463
|
const lengthResult = getLengthOverflowResult(uiState, {
|
|
31086
31464
|
maxLength: maxLengthGuard,
|
|
31465
|
+
uiStateNow,
|
|
31087
31466
|
});
|
|
31088
31467
|
if (lengthResult) {
|
|
31089
31468
|
show(lengthResult.message, e);
|
|
@@ -35111,13 +35490,15 @@ const splitControlProps = props => {
|
|
|
35111
35490
|
};
|
|
35112
35491
|
const controlRootProps = {};
|
|
35113
35492
|
for (const key of Object.keys(props)) {
|
|
35114
|
-
|
|
35115
|
-
|
|
35116
|
-
|
|
35117
|
-
|
|
35118
|
-
}
|
|
35119
|
-
|
|
35493
|
+
const attributeName = controlAttributeFromProp(key);
|
|
35494
|
+
if (attributeName) {
|
|
35495
|
+
controlHostProps[attributeName] = props[key];
|
|
35496
|
+
continue;
|
|
35497
|
+
}
|
|
35498
|
+
if (isControlProp(key)) {
|
|
35499
|
+
continue;
|
|
35120
35500
|
}
|
|
35501
|
+
controlRootProps[key] = props[key];
|
|
35121
35502
|
}
|
|
35122
35503
|
return [controlRootProps, controlHostProps];
|
|
35123
35504
|
};
|
|
@@ -37217,6 +37598,7 @@ const TextAnchor = ({
|
|
|
37217
37598
|
ref: anchorRef,
|
|
37218
37599
|
className: "navi_text_anchor",
|
|
37219
37600
|
"aria-hidden": "true",
|
|
37601
|
+
hidden: true,
|
|
37220
37602
|
children: "\u200B"
|
|
37221
37603
|
})]
|
|
37222
37604
|
});
|
|
@@ -37884,8 +38266,10 @@ const shouldInjectSpacingBetween = (left, right) => {
|
|
|
37884
38266
|
* @param {boolean} [emojiAsIcon]
|
|
37885
38267
|
* Renders every emoji found in the string children as an `Icon`, so it sits
|
|
37886
38268
|
* in the line like a character and never makes the line taller than the
|
|
37887
|
-
* text. For free text a user typed (a message, a description)
|
|
37888
|
-
*
|
|
38269
|
+
* text. For free text a user typed (a message, a description); see
|
|
38270
|
+
* `docs/typography.md`. Only the strings this `Text` receives are rewritten —
|
|
38271
|
+
* a string a child component renders is out of reach, and that component
|
|
38272
|
+
* calls `renderEmojiAsIcon()` itself instead.
|
|
37889
38273
|
*
|
|
37890
38274
|
* @param {boolean} [preventSpaceUnderlines]
|
|
37891
38275
|
* Replaces real space characters between children with padding-based spaces.
|
|
@@ -42702,7 +43086,16 @@ const createToggleEvent = open => {
|
|
|
42702
43086
|
return toggleEvent;
|
|
42703
43087
|
};
|
|
42704
43088
|
|
|
42705
|
-
const DEFAULT_VALIDITY_STATE = { valid: true };
|
|
43089
|
+
const DEFAULT_VALIDITY_STATE = { valid: true, reported: null };
|
|
43090
|
+
|
|
43091
|
+
/**
|
|
43092
|
+
* The control's constraint validity, re-read whenever it changes:
|
|
43093
|
+
* `{ valid, reported, [constraintName]: null | failureInfo }`.
|
|
43094
|
+
*
|
|
43095
|
+
* `reported` names the constraint whose message the callout shows — the one
|
|
43096
|
+
* navi picks when several fail at once. A failure info carries `messageString`,
|
|
43097
|
+
* the sentence itself.
|
|
43098
|
+
*/
|
|
42706
43099
|
const useConstraintValidityState = (ref) => {
|
|
42707
43100
|
const checkValue = () => {
|
|
42708
43101
|
const element = ref.current;
|
|
@@ -46380,8 +46773,13 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
46380
46773
|
* "postal" → postal code (digits, letters, space, hyphen)
|
|
46381
46774
|
* "iban" → IBAN (uppercase and digits)
|
|
46382
46775
|
* "slug" → URL slug (lowercase, digits, hyphens)
|
|
46383
|
-
* "
|
|
46776
|
+
* "noEmoji" → anything but an emoji
|
|
46777
|
+
* "[A-Z0-9]" → any custom regex character class, compiled with the `u`
|
|
46778
|
+
* flag: `\p{...}` is available, and an emoji counts as one
|
|
46779
|
+
* character rather than two halves.
|
|
46384
46780
|
* inputMode and pattern are auto-derived from the preset when not explicitly set.
|
|
46781
|
+
* The presets come from @jsenv/validity, so the same name names the class a
|
|
46782
|
+
* server checks the value against (see docs/field_validation.md).
|
|
46385
46783
|
*
|
|
46386
46784
|
* - maxLengthGuard — combines maxLength + overflow guard in one prop.
|
|
46387
46785
|
* Blocks keydown when the limit is reached; truncates on paste/set with an info callout.
|
|
@@ -59043,10 +59441,11 @@ const PendingScrollRefContext = createContext(null);
|
|
|
59043
59441
|
// "muted" — keep in DOM, visible but opacified and still interactive
|
|
59044
59442
|
const SearchNoMatchModeContext = createContext("remove");
|
|
59045
59443
|
|
|
59046
|
-
//
|
|
59047
|
-
//
|
|
59048
|
-
//
|
|
59049
|
-
//
|
|
59444
|
+
// How many rows the list draws at once. Past that, a render window [start, end)
|
|
59445
|
+
// caps the number of DOM nodes: the window slides as the user scrolls, using
|
|
59446
|
+
// actual DOM positions (getBoundingClientRect) to find the first visible item —
|
|
59447
|
+
// no height estimation. It frames the rows a run draws (see ListItems); items
|
|
59448
|
+
// declared one by one (<List.Item>) are all drawn, whatever the budget.
|
|
59050
59449
|
const RENDER_BUDGET_DEFAULT = 100;
|
|
59051
59450
|
|
|
59052
59451
|
// Attribute used on <li> elements rendered by ListItemReal so the scroll listener
|
|
@@ -59055,8 +59454,9 @@ const REAL_LIST_ITEM_SELECTOR = `[navi-list-item-real]`;
|
|
|
59055
59454
|
// Rows standing in for content that has not arrived (see List's renderSkeleton).
|
|
59056
59455
|
const SKELETON_LIST_ITEM_CLASS = "navi_list_item_skeleton";
|
|
59057
59456
|
|
|
59058
|
-
// Carries the render window {start, end}
|
|
59059
|
-
//
|
|
59457
|
+
// Carries the render window {start, end} from List down to the runs of rows
|
|
59458
|
+
// inside it (see ListItems): a run draws the rows it frames and holds the room
|
|
59459
|
+
// of the others.
|
|
59060
59460
|
const RenderWindowContext = createContext(null);
|
|
59061
59461
|
// Carries List's own `columns` prop (a grid-template-columns value, e.g.
|
|
59062
59462
|
// "1fr auto auto") down to each ListItem/filler/fallback so they can render
|
|
@@ -59893,12 +60293,46 @@ const ListUI = props => {
|
|
|
59893
60293
|
searchText,
|
|
59894
60294
|
horizontal
|
|
59895
60295
|
});
|
|
60296
|
+
|
|
60297
|
+
// renderBudget frames the rows of a run; a list whose items are all declared
|
|
60298
|
+
// one by one draws every one of them, and the prop looks exactly like it is
|
|
60299
|
+
// doing something. Said once per list, when there is something drawn to
|
|
60300
|
+
// judge it on — a run mounting later (rows behind a loading state) is not a
|
|
60301
|
+
// list without one.
|
|
60302
|
+
const renderBudgetWarnedRef = useRef(false);
|
|
60303
|
+
useLayoutEffect(() => {
|
|
60304
|
+
if (props.renderBudget === undefined || renderBudgetWarnedRef.current) {
|
|
60305
|
+
return;
|
|
60306
|
+
}
|
|
60307
|
+
if (virtual.hasRuns() || tracker.itemsSignal.peek().length === 0) {
|
|
60308
|
+
return;
|
|
60309
|
+
}
|
|
60310
|
+
renderBudgetWarnedRef.current = true;
|
|
60311
|
+
console.warn(`List: renderBudget=${renderBudget} has no effect here. The render window frames the rows a run draws (<List.Items itemsAction>); items declared one by one (<List.Item>) are all rendered. Move the items to <List.Items> to cap the number of DOM nodes, or drop the prop.`);
|
|
60312
|
+
});
|
|
59896
60313
|
virtual.captureAnchor = captureAnchor;
|
|
59897
60314
|
virtual.virtualItemSizeSignal = virtualItemSizeSignal;
|
|
59898
60315
|
virtual.horizontal = Boolean(horizontal);
|
|
59899
60316
|
virtual.renderSkeleton = renderSkeleton;
|
|
60317
|
+
|
|
60318
|
+
// A row is addressed by id from outside (--navi-scroll, --navi-select): the
|
|
60319
|
+
// ones drawn have registered themselves with the tracker, and the ones a run
|
|
60320
|
+
// holds without drawing are known only to that run (see List.Items' row
|
|
60321
|
+
// locator). Both answer here, so a row is reachable whether or not the
|
|
60322
|
+
// window happens to frame it.
|
|
59900
60323
|
const getItemById = itemId => {
|
|
59901
|
-
|
|
60324
|
+
const itemDrawn = tracker.itemsSignal.peek().find(item => item.id === itemId);
|
|
60325
|
+
if (itemDrawn) {
|
|
60326
|
+
return itemDrawn;
|
|
60327
|
+
}
|
|
60328
|
+
const rowIndex = virtual.locateRow(itemId);
|
|
60329
|
+
if (rowIndex === null) {
|
|
60330
|
+
return undefined;
|
|
60331
|
+
}
|
|
60332
|
+
return {
|
|
60333
|
+
id: itemId,
|
|
60334
|
+
index: rowIndex
|
|
60335
|
+
};
|
|
59902
60336
|
};
|
|
59903
60337
|
const noMatchCount = tracker.noMatchCountSignal.value;
|
|
59904
60338
|
// What the list stands for, which is not always what it holds: a run saying
|
|
@@ -62438,6 +62872,9 @@ const createListVirtual = () => {
|
|
|
62438
62872
|
passId++;
|
|
62439
62873
|
nextIndex = 0;
|
|
62440
62874
|
},
|
|
62875
|
+
// Whether any run of rows lives in this list: what makes a render window
|
|
62876
|
+
// mean anything (see List's renderBudget).
|
|
62877
|
+
hasRuns: () => locatorByOwner.size > 0,
|
|
62441
62878
|
setRowLocator: (ownerId, locate) => {
|
|
62442
62879
|
locatorByOwner.set(ownerId, locate);
|
|
62443
62880
|
},
|
|
@@ -62484,12 +62921,17 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62484
62921
|
* for them — which is what makes an infinitely scrolled list nothing more than
|
|
62485
62922
|
* a list that says how many rows it has.
|
|
62486
62923
|
*
|
|
62487
|
-
*
|
|
62488
|
-
*
|
|
62489
|
-
*
|
|
62490
|
-
*
|
|
62491
|
-
*
|
|
62492
|
-
*
|
|
62924
|
+
* A collection held in memory is given whole: `items={rows}`. The run holds all
|
|
62925
|
+
* of them from the first render and never asks for anything — it is still the
|
|
62926
|
+
* render window that decides how many are drawn.
|
|
62927
|
+
*
|
|
62928
|
+
* A collection read a slice at a time comes from `itemsAction(range)`: the run
|
|
62929
|
+
* asks for what it is about to draw and keeps what it gets. The range says the
|
|
62930
|
+
* same thing three ways, so a source can read it however it paginates —
|
|
62931
|
+
* `{ start, end }` (places in the collection, a negative `start` counting back
|
|
62932
|
+
* from the end like `Range: items=-25`, which is what a list opening on its
|
|
62933
|
+
* last rows asks for before it knows how many there are), `limit` (how many
|
|
62934
|
+
* rows), and
|
|
62493
62935
|
* `before`/`after`/`around` (the id of a row to count from, for a source
|
|
62494
62936
|
* paginating by cursor). Answer with the rows (an array — that is all of
|
|
62495
62937
|
* them), or with a range the way a Content-Range does: `{ items, start, count }`
|
|
@@ -62503,11 +62945,11 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62503
62945
|
* copies of the JSON. The list holds the slices, the store holds the objects
|
|
62504
62946
|
* (see docs/resource.md).
|
|
62505
62947
|
*
|
|
62506
|
-
* A collection
|
|
62507
|
-
*
|
|
62508
|
-
*
|
|
62509
|
-
* with it, the way one does for anything else that is not the same
|
|
62510
|
-
* anymore.
|
|
62948
|
+
* A collection that changes as a whole (a search reordering it) is a different
|
|
62949
|
+
* collection: with `items`, another array is another collection and the run
|
|
62950
|
+
* draws it from its first row; with `itemsAction`, give the run a `key` that
|
|
62951
|
+
* changes with it, the way one does for anything else that is not the same
|
|
62952
|
+
* thing anymore.
|
|
62511
62953
|
*
|
|
62512
62954
|
* A row says what it is where it is drawn: `renderItem` returns a
|
|
62513
62955
|
* `<List.Item>` carrying its own props (`selectable`, `value`, `selected`…),
|
|
@@ -62519,7 +62961,8 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62519
62961
|
*
|
|
62520
62962
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
62521
62963
|
* renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
|
|
62522
|
-
*
|
|
62964
|
+
* items?: any[],
|
|
62965
|
+
* itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
|
|
62523
62966
|
* count?: number,
|
|
62524
62967
|
* groupBy?: (item: any, index: number) => any,
|
|
62525
62968
|
* renderGroupLabel?: (item: any, index: number) => import("ignore:preact").ComponentChildren,
|
|
@@ -62534,6 +62977,13 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62534
62977
|
* What one row is, given the item and where it sits. `state.refreshing` says
|
|
62535
62978
|
* the rows drawn are the ones from before while the run reads the collection
|
|
62536
62979
|
* again — the list carries `navi-refreshing` for the same reason.
|
|
62980
|
+
* @param {any[]} [props.items]
|
|
62981
|
+
* The collection, when it is held in memory: all of it, in order. Nothing is
|
|
62982
|
+
* ever asked for — `itemsAction`, `count`, `pageSize` and `memoryBudget` have
|
|
62983
|
+
* no part to play, and no row is ever a skeleton.
|
|
62984
|
+
* @param {(range: object) => any} [props.itemsAction]
|
|
62985
|
+
* Where the rows come from when the collection is read a slice at a time:
|
|
62986
|
+
* a resource's range reader (`RESOURCE.GET_RANGE.bindParams(...)`).
|
|
62537
62987
|
* @param {(item: any, index: number) => any} [props.groupBy]
|
|
62538
62988
|
* What tells rows that belong together apart from the others — the day of a
|
|
62539
62989
|
* message, the month of a game. Consecutive rows sharing it are wrapped in a
|
|
@@ -62556,7 +63006,8 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62556
63006
|
* How many rows the run keeps in memory. Past that, the ones far from what is
|
|
62557
63007
|
* on screen are dropped (and asked for again if the user goes back) — the
|
|
62558
63008
|
* same trade the render window makes with the DOM, one order of magnitude
|
|
62559
|
-
* further out. `0` keeps
|
|
63009
|
+
* further out. `Infinity` keeps every row the run ever received; `0` keeps
|
|
63010
|
+
* only the ones around the window.
|
|
62560
63011
|
* @param {false|(index: number) => any} [props.renderSkeleton]
|
|
62561
63012
|
* What to draw for a row the run does not hold. Defaults to List's own
|
|
62562
63013
|
* `renderSkeleton`, then to a bare `<List.Item skeleton>`; `false` leaves the
|
|
@@ -62577,6 +63028,7 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62577
63028
|
*/
|
|
62578
63029
|
const ListItems = ({
|
|
62579
63030
|
renderItem,
|
|
63031
|
+
items,
|
|
62580
63032
|
itemsAction,
|
|
62581
63033
|
count,
|
|
62582
63034
|
pageSize,
|
|
@@ -62593,6 +63045,7 @@ const ListItems = ({
|
|
|
62593
63045
|
const renderWindow = useContext(RenderWindowContext);
|
|
62594
63046
|
const separator = useContext(SeparatorContext);
|
|
62595
63047
|
const store = useItemStore({
|
|
63048
|
+
items,
|
|
62596
63049
|
count,
|
|
62597
63050
|
itemsAction,
|
|
62598
63051
|
memoryBudget,
|
|
@@ -62883,11 +63336,17 @@ const rangeIsSame = (a, b) => {
|
|
|
62883
63336
|
// are in all is enough to place it, so the pages need not be contiguous nor
|
|
62884
63337
|
// arrive in order.
|
|
62885
63338
|
const useItemStore = ({
|
|
63339
|
+
items,
|
|
62886
63340
|
count,
|
|
62887
63341
|
itemsAction,
|
|
62888
63342
|
memoryBudget,
|
|
62889
63343
|
onRequestStateChange
|
|
62890
63344
|
}) => {
|
|
63345
|
+
// A collection given whole (`items`) is held from the first render: nothing
|
|
63346
|
+
// to ask for, nothing to keep across mounts, nothing to invalidate. The rest
|
|
63347
|
+
// of the store then never has a hole to fill, so it stays inert on its own —
|
|
63348
|
+
// the asking below finds nothing missing.
|
|
63349
|
+
const inMemory = items !== undefined;
|
|
62891
63350
|
// The run's asking, on the same channel as the window it asks for — they are
|
|
62892
63351
|
// one subject: what the list is about to draw is what it goes to fetch (see
|
|
62893
63352
|
// `useRequestMissing`, and `updateRenderWindow` which logs the other half).
|
|
@@ -62909,8 +63368,28 @@ const useItemStore = ({
|
|
|
62909
63368
|
// the reader holds for it (see resource_range_reader.js).
|
|
62910
63369
|
useRef(false);
|
|
62911
63370
|
const pagesRef = useRef(null);
|
|
63371
|
+
const itemsRef = useRef(items);
|
|
63372
|
+
const itemsHeldRef = useRef(false);
|
|
62912
63373
|
let restored = false;
|
|
62913
|
-
if (
|
|
63374
|
+
if (inMemory) {
|
|
63375
|
+
// The array as a whole is the collection: another array is another
|
|
63376
|
+
// collection, drawn from its first row (which is also why a run reading a
|
|
63377
|
+
// collection that changes as a whole takes a key).
|
|
63378
|
+
if (!pagesRef.current || itemsRef.current !== items) {
|
|
63379
|
+
itemsRef.current = items;
|
|
63380
|
+
const byIndex = new Map();
|
|
63381
|
+
let index = 0;
|
|
63382
|
+
while (index < items.length) {
|
|
63383
|
+
byIndex.set(index, items[index]);
|
|
63384
|
+
index++;
|
|
63385
|
+
}
|
|
63386
|
+
pagesRef.current = {
|
|
63387
|
+
byIndex,
|
|
63388
|
+
count: items.length
|
|
63389
|
+
};
|
|
63390
|
+
itemsHeldRef.current = false;
|
|
63391
|
+
}
|
|
63392
|
+
} else if (!pagesRef.current) {
|
|
62914
63393
|
const composition = typeof itemsAction === "function" && itemsAction.readComposition ? itemsAction.readComposition() : null;
|
|
62915
63394
|
if (composition && composition.count !== undefined) {
|
|
62916
63395
|
pagesRef.current = composition;
|
|
@@ -62957,6 +63436,16 @@ const useItemStore = ({
|
|
|
62957
63436
|
// range askable again (see the request memory just above).
|
|
62958
63437
|
const [failure, setFailure] = useState(null);
|
|
62959
63438
|
const virtual = useContext(ListVirtualContext);
|
|
63439
|
+
// The rows are there, which is what the list waits for to place itself on the
|
|
63440
|
+
// row it is held at (see placeWhereHeld). Said from an effect: a signal read
|
|
63441
|
+
// during this very render must not be written during it.
|
|
63442
|
+
useLayoutEffect(() => {
|
|
63443
|
+
if (!inMemory || itemsHeldRef.current) {
|
|
63444
|
+
return;
|
|
63445
|
+
}
|
|
63446
|
+
itemsHeldRef.current = true;
|
|
63447
|
+
virtual.pagesSignal.value = virtual.pagesSignal.peek() + 1;
|
|
63448
|
+
});
|
|
62960
63449
|
// Before the first answer a run does not know how many rows it stands for.
|
|
62961
63450
|
// It stands for a windowful of them: a list that is about to be filled looks
|
|
62962
63451
|
// like rows on their way, not like an empty list.
|
|
@@ -63029,10 +63518,12 @@ const useItemStore = ({
|
|
|
63029
63518
|
// dropped and simply asked for again if the user goes back — the same
|
|
63030
63519
|
// trade the render window makes, one order of magnitude further out.
|
|
63031
63520
|
forget: (windowFrom, windowTo) => {
|
|
63032
|
-
|
|
63033
|
-
|
|
63521
|
+
if (inMemory) {
|
|
63522
|
+
// Dropping a row here would drop it for good: there is no source to
|
|
63523
|
+
// ask it back from.
|
|
63034
63524
|
return;
|
|
63035
63525
|
}
|
|
63526
|
+
const budget = memoryBudget === undefined ? ITEM_STORE_MAX_DEFAULT : memoryBudget;
|
|
63036
63527
|
const keepFrom = windowFrom - ITEM_STORE_KEEP_AROUND;
|
|
63037
63528
|
const keepTo = windowTo + ITEM_STORE_KEEP_AROUND;
|
|
63038
63529
|
if (pages.byIndex.size > budget) {
|
|
@@ -67469,11 +67960,18 @@ const css$r = /* css */`
|
|
|
67469
67960
|
99999 fallback means "no cap" without needing a conditional rule. */
|
|
67470
67961
|
max-height: calc(var(--textarea-max-rows, 99999) * 1lh);
|
|
67471
67962
|
field-sizing: content;
|
|
67472
|
-
/* Explicit, never normal
|
|
67473
|
-
|
|
67474
|
-
|
|
67475
|
-
|
|
67476
|
-
|
|
67963
|
+
/* Explicit, never normal, for two independent reasons.
|
|
67964
|
+
minRows/maxRows are lengths in lh, and with line-height normal the lh
|
|
67965
|
+
unit resolves to a theoretical value that does not match the real
|
|
67966
|
+
rendered line — the box then jumps by a few pixels the moment the first
|
|
67967
|
+
character replaces the theory with a real line.
|
|
67968
|
+
And a line box under "normal" takes the height of the tallest font it
|
|
67969
|
+
holds, so the one line carrying an emoji stands taller than the ones
|
|
67970
|
+
around it — here, where the text is typed and no glyph can be wrapped
|
|
67971
|
+
the way emojiAsIcon wraps one, the line height is the only lever.
|
|
67972
|
+
1.5 is also tall enough to contain an emoji's own box, so it is not
|
|
67973
|
+
clipped either; a tighter value would keep the rows even and cut the
|
|
67974
|
+
glyph. See docs/typography.md. */
|
|
67477
67975
|
line-height: 1.5;
|
|
67478
67976
|
/* The control grows itself; resizable below hands the handle back. */
|
|
67479
67977
|
resize: none;
|
|
@@ -78739,5 +79237,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
78739
79237
|
})
|
|
78740
79238
|
});
|
|
78741
79239
|
|
|
78742
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, renderEmojiAsIcon, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
|
|
79240
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, renderEmojiAsIcon, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
|
|
78743
79241
|
//# sourceMappingURL=jsenv_navi.js.map
|