@jsenv/navi 0.29.109 → 0.29.110
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 +360 -53
- package/dist/jsenv_navi.js.map +35 -28
- package/docs/actions.md +7 -0
- package/docs/control_group.md +9 -0
- package/docs/error_handling.md +14 -0
- package/docs/popup_open.md +9 -2
- package/docs/typography.md +24 -3
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
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";
|
|
@@ -3689,6 +3689,7 @@ const CONSTRAINT_NAME_TO_PROP = {
|
|
|
3689
3689
|
min: "minMessage",
|
|
3690
3690
|
max: "maxMessage",
|
|
3691
3691
|
single_space: "singleSpaceMessage",
|
|
3692
|
+
displayable: "displayableMessage",
|
|
3692
3693
|
same_as: "sameAsMessage",
|
|
3693
3694
|
min_lower_letter: "minLowerLetterMessage",
|
|
3694
3695
|
min_upper_letter: "minUpperLetterMessage",
|
|
@@ -3820,6 +3821,11 @@ const CONTROL_PROP_SET = new Set([
|
|
|
3820
3821
|
|
|
3821
3822
|
"loading",
|
|
3822
3823
|
"basePseudoState",
|
|
3824
|
+
// App constraints this control must satisfy, on top of the ones navi ships:
|
|
3825
|
+
// `constraints={[MY_CONSTRAINT]}`. A constraint is an object, so it carries
|
|
3826
|
+
// its own parameters — nothing has to travel through an attribute. Read on
|
|
3827
|
+
// every check in control_validation.js; `registerGlobalConstraint` is the
|
|
3828
|
+
// same thing for every control at once.
|
|
3823
3829
|
"constraints",
|
|
3824
3830
|
|
|
3825
3831
|
// A real target inside a zone that belongs to another control — see
|
|
@@ -7245,6 +7251,18 @@ naviI18n.addAll({
|
|
|
7245
7251
|
fr: "Ce champ ne doit pas contenir plusieurs espaces consécutifs.",
|
|
7246
7252
|
en: "This field must not contain consecutive spaces.",
|
|
7247
7253
|
},
|
|
7254
|
+
"constraint.displayable.stacked_marks.default": {
|
|
7255
|
+
fr: "Ce champ ne doit pas empiler plus de <strong>[max]</strong> signes sur un même caractère.",
|
|
7256
|
+
en: "This field must not stack more than <strong>[max]</strong> marks on a single character.",
|
|
7257
|
+
},
|
|
7258
|
+
"constraint.displayable.invisible.default": {
|
|
7259
|
+
fr: "Ce champ doit contenir au moins un caractère visible.",
|
|
7260
|
+
en: "This field must contain at least one visible character.",
|
|
7261
|
+
},
|
|
7262
|
+
"constraint.displayable.blank_lines.default": {
|
|
7263
|
+
fr: "Ce champ ne doit pas contenir plusieurs lignes vides consécutives.",
|
|
7264
|
+
en: "This field must not contain consecutive blank lines.",
|
|
7265
|
+
},
|
|
7248
7266
|
"constraint.min_lower_letter.password.singular": {
|
|
7249
7267
|
fr: "Ce mot de passe doit contenir au moins une lettre minuscule.",
|
|
7250
7268
|
en: "This password must contain at least one lowercase letter.",
|
|
@@ -10969,6 +10987,91 @@ const isFunctionButNotAnActionFunction = (action) => {
|
|
|
10969
10987
|
return typeof action === "function" && !action.isAction;
|
|
10970
10988
|
};
|
|
10971
10989
|
|
|
10990
|
+
/**
|
|
10991
|
+
* `data-displayable` — the value must be something the layout can actually
|
|
10992
|
+
* draw. Three shapes break a row, a card or a list even though every character
|
|
10993
|
+
* taken alone is legitimate, so no character class can express them:
|
|
10994
|
+
*
|
|
10995
|
+
* - marks stacked on one base character ("zalgo"): a diacritic is a normal
|
|
10996
|
+
* character — a decomposed Vietnamese letter carries two, a vocalized Hebrew
|
|
10997
|
+
* one three — what is not normal is the count in a row. Thirty of them draw
|
|
10998
|
+
* far above the line, over the row above.
|
|
10999
|
+
* - a value that is not empty and yet shows nothing: only spaces, only marks,
|
|
11000
|
+
* only format characters. An empty-looking line in the middle of a list
|
|
11001
|
+
* reads as a bug.
|
|
11002
|
+
* - blank lines in series: forty newlines make a card as tall as the screen.
|
|
11003
|
+
*
|
|
11004
|
+
* These are display rules, not app rules: they hold for every field, whatever
|
|
11005
|
+
* it holds — which is why they ship here rather than being rewritten per app.
|
|
11006
|
+
*
|
|
11007
|
+
* Note what is deliberately NOT refused: U+200D (ZWJ) and U+200C (ZWNJ) are
|
|
11008
|
+
* invisible characters, but the first assembles 👨👩👧 and 🏳️🌈 and the second
|
|
11009
|
+
* separates two letters in Persian. Banning invisible characters outright
|
|
11010
|
+
* would mean banning composed emoji. They only make a value fail here when
|
|
11011
|
+
* nothing visible is left once they are removed.
|
|
11012
|
+
*/
|
|
11013
|
+
|
|
11014
|
+
|
|
11015
|
+
// Above what any writing system needs on one base character, far below what
|
|
11016
|
+
// zalgo uses. Raise it with data-max-stacked-marks when a language needs more.
|
|
11017
|
+
const DEFAULT_MAX_STACKED_MARKS = 5;
|
|
11018
|
+
|
|
11019
|
+
// Everything that occupies no ink of its own: spaces, control and format
|
|
11020
|
+
// characters, and combining marks (which draw on a base character, so a value
|
|
11021
|
+
// made only of them has nothing to draw on).
|
|
11022
|
+
const INK_LESS_REGEX = /[\p{White_Space}\p{Cc}\p{Cf}\p{M}]/gu;
|
|
11023
|
+
// Two newlines are one blank line — a paragraph break; three are two.
|
|
11024
|
+
const BLANK_LINES_REGEX = /\n[^\S\n]*\n[^\S\n]*\n/;
|
|
11025
|
+
|
|
11026
|
+
const stackedMarksRegexCache = new Map();
|
|
11027
|
+
const getStackedMarksRegex = (maxStackedMarks) => {
|
|
11028
|
+
const fromCache = stackedMarksRegexCache.get(maxStackedMarks);
|
|
11029
|
+
if (fromCache) {
|
|
11030
|
+
return fromCache;
|
|
11031
|
+
}
|
|
11032
|
+
const regex = new RegExp(`\\p{M}{${maxStackedMarks + 1},}`, "u");
|
|
11033
|
+
stackedMarksRegexCache.set(maxStackedMarks, regex);
|
|
11034
|
+
return regex;
|
|
11035
|
+
};
|
|
11036
|
+
|
|
11037
|
+
const DISPLAYABLE_CONSTRAINT = {
|
|
11038
|
+
name: "displayable",
|
|
11039
|
+
messageAttribute: "data-displayable-message",
|
|
11040
|
+
check: (field) => {
|
|
11041
|
+
const displayable = field.controlHostProps["data-displayable"];
|
|
11042
|
+
if (displayable === undefined) {
|
|
11043
|
+
return null;
|
|
11044
|
+
}
|
|
11045
|
+
const valueAsString =
|
|
11046
|
+
field.uiState === undefined ? "" : String(field.uiState);
|
|
11047
|
+
if (valueAsString === "") {
|
|
11048
|
+
// An empty field is `required`'s business, not this one's.
|
|
11049
|
+
return null;
|
|
11050
|
+
}
|
|
11051
|
+
|
|
11052
|
+
const maxStackedMarksAttribute =
|
|
11053
|
+
field.controlHostProps["data-max-stacked-marks"];
|
|
11054
|
+
const maxStackedMarks =
|
|
11055
|
+
maxStackedMarksAttribute === undefined
|
|
11056
|
+
? DEFAULT_MAX_STACKED_MARKS
|
|
11057
|
+
: parseInt(maxStackedMarksAttribute, 10);
|
|
11058
|
+
if (getStackedMarksRegex(maxStackedMarks).test(valueAsString)) {
|
|
11059
|
+
return naviI18n("constraint.displayable.stacked_marks.default", {
|
|
11060
|
+
max: maxStackedMarks,
|
|
11061
|
+
});
|
|
11062
|
+
}
|
|
11063
|
+
if (valueAsString.replace(INK_LESS_REGEX, "") === "") {
|
|
11064
|
+
return naviI18n("constraint.displayable.invisible.default");
|
|
11065
|
+
}
|
|
11066
|
+
if (BLANK_LINES_REGEX.test(valueAsString)) {
|
|
11067
|
+
return naviI18n("constraint.displayable.blank_lines.default");
|
|
11068
|
+
}
|
|
11069
|
+
return null;
|
|
11070
|
+
},
|
|
11071
|
+
};
|
|
11072
|
+
CONSTRAINT_ATTRIBUTE_SET.add("data-displayable");
|
|
11073
|
+
CONSTRAINT_ATTRIBUTE_SET.add("data-max-stacked-marks");
|
|
11074
|
+
|
|
10972
11075
|
const MIN_LOWER_LETTER_CONSTRAINT = {
|
|
10973
11076
|
name: "min_lower_letter",
|
|
10974
11077
|
messageAttribute: "data-min-lower-letter-message",
|
|
@@ -11386,6 +11489,7 @@ const STANDARD_CONSTRAINT_SET = new Set([
|
|
|
11386
11489
|
const NAVI_CONSTRAINT_SET = new Set([
|
|
11387
11490
|
MIN_SPECIAL_CHAR_CONSTRAINT,
|
|
11388
11491
|
SINGLE_SPACE_CONSTRAINT,
|
|
11492
|
+
DISPLAYABLE_CONSTRAINT,
|
|
11389
11493
|
MIN_DIGIT_CONSTRAINT,
|
|
11390
11494
|
MIN_UPPER_LETTER_CONSTRAINT,
|
|
11391
11495
|
MIN_LOWER_LETTER_CONSTRAINT,
|
|
@@ -11398,8 +11502,29 @@ const DEFAULT_CONSTRAINT_SET = new Set([
|
|
|
11398
11502
|
...NAVI_CONSTRAINT_SET,
|
|
11399
11503
|
]);
|
|
11400
11504
|
const registerGlobalConstraint = (customConstraint) => {
|
|
11401
|
-
|
|
11402
|
-
|
|
11505
|
+
const constraint = normalizeConstraint(customConstraint);
|
|
11506
|
+
NAVI_CONSTRAINT_SET.add(constraint);
|
|
11507
|
+
DEFAULT_CONSTRAINT_SET.add(constraint);
|
|
11508
|
+
};
|
|
11509
|
+
|
|
11510
|
+
// A constraint may be written as a bare check function; the rest of the code
|
|
11511
|
+
// wants the object shape. The wrapper is cached so a function passed on every
|
|
11512
|
+
// render keeps one identity across checks.
|
|
11513
|
+
const constraintFromFunctionMap = new WeakMap();
|
|
11514
|
+
const normalizeConstraint = (constraint) => {
|
|
11515
|
+
if (typeof constraint !== "function") {
|
|
11516
|
+
return constraint;
|
|
11517
|
+
}
|
|
11518
|
+
const existing = constraintFromFunctionMap.get(constraint);
|
|
11519
|
+
if (existing) {
|
|
11520
|
+
return existing;
|
|
11521
|
+
}
|
|
11522
|
+
const constraintObject = {
|
|
11523
|
+
name: constraint.name || "custom_function",
|
|
11524
|
+
check: constraint,
|
|
11525
|
+
};
|
|
11526
|
+
constraintFromFunctionMap.set(constraint, constraintObject);
|
|
11527
|
+
return constraintObject;
|
|
11403
11528
|
};
|
|
11404
11529
|
|
|
11405
11530
|
const createControlValidation = (
|
|
@@ -11415,15 +11540,10 @@ const createControlValidation = (
|
|
|
11415
11540
|
const dynamicConstraintSet = new Set();
|
|
11416
11541
|
{
|
|
11417
11542
|
controlValidity.registerConstraint = (constraint) => {
|
|
11418
|
-
|
|
11419
|
-
|
|
11420
|
-
name: constraint.name || "custom_function",
|
|
11421
|
-
check: constraint,
|
|
11422
|
-
};
|
|
11423
|
-
}
|
|
11424
|
-
dynamicConstraintSet.add(constraint);
|
|
11543
|
+
const constraintObject = normalizeConstraint(constraint);
|
|
11544
|
+
dynamicConstraintSet.add(constraintObject);
|
|
11425
11545
|
return () => {
|
|
11426
|
-
dynamicConstraintSet.delete(
|
|
11546
|
+
dynamicConstraintSet.delete(constraintObject);
|
|
11427
11547
|
};
|
|
11428
11548
|
};
|
|
11429
11549
|
}
|
|
@@ -11480,6 +11600,16 @@ const createControlValidation = (
|
|
|
11480
11600
|
...DEFAULT_CONSTRAINT_SET,
|
|
11481
11601
|
...dynamicConstraintSet,
|
|
11482
11602
|
]);
|
|
11603
|
+
// An app constraint declared at the call site: `constraints={[MY_CONSTRAINT]}`.
|
|
11604
|
+
// Read from the raw props on every check so a constraint whose parameters
|
|
11605
|
+
// are closed over is re-created freely, and last so the constraints navi
|
|
11606
|
+
// ships are the ones reported first (see pickConstraintFailureInfo).
|
|
11607
|
+
const constraintsFromProps = controller.props.constraints;
|
|
11608
|
+
if (constraintsFromProps) {
|
|
11609
|
+
for (const constraintFromProps of constraintsFromProps) {
|
|
11610
|
+
constraintSet.add(normalizeConstraint(constraintFromProps));
|
|
11611
|
+
}
|
|
11612
|
+
}
|
|
11483
11613
|
const elementSig = getElementSignature(controller.ref.current);
|
|
11484
11614
|
// Not logged: every control checks its constraints on every interaction and
|
|
11485
11615
|
// almost always passes, so this line alone was most of the debug output —
|
|
@@ -12807,12 +12937,18 @@ const useArraySignalMembership = (...args) => {
|
|
|
12807
12937
|
"useArraySignalMembership requires at least 2 arguments: [arraySignal, id]",
|
|
12808
12938
|
);
|
|
12809
12939
|
}
|
|
12940
|
+
const [arraySignal, id] = args;
|
|
12810
12941
|
|
|
12811
|
-
|
|
12812
|
-
|
|
12813
|
-
|
|
12814
|
-
|
|
12815
|
-
|
|
12942
|
+
// Through a computed so the component re-renders when its own membership
|
|
12943
|
+
// changes, not every time anything else is added to or removed from the
|
|
12944
|
+
// array: a list of 200 rows each watching the same array would otherwise all
|
|
12945
|
+
// re-render (and each re-scan the array) when one row is toggled.
|
|
12946
|
+
const isMember = useComputed(() => arraySignal.value.includes(id)).value;
|
|
12947
|
+
const [, add, remove] = useMemo(
|
|
12948
|
+
() => arraySignalMembership(arraySignal, id),
|
|
12949
|
+
[arraySignal, id],
|
|
12950
|
+
);
|
|
12951
|
+
return [isMember, add, remove];
|
|
12816
12952
|
};
|
|
12817
12953
|
|
|
12818
12954
|
const arraySignalMembership = (...args) => {
|
|
@@ -14169,7 +14305,7 @@ const createRangeReader = (
|
|
|
14169
14305
|
// dropped here is one that gets asked for again after a remount.
|
|
14170
14306
|
readRange.trimComposition = (keepFrom, keepTo, budget) => {
|
|
14171
14307
|
const composition = findComposition(currentParams());
|
|
14172
|
-
if (!composition ||
|
|
14308
|
+
if (!composition || composition.idByIndex.size <= budget) {
|
|
14173
14309
|
return;
|
|
14174
14310
|
}
|
|
14175
14311
|
for (const index of composition.idByIndex.keys()) {
|
|
@@ -19726,6 +19862,21 @@ import.meta.css = [/* css */`
|
|
|
19726
19862
|
}
|
|
19727
19863
|
}
|
|
19728
19864
|
|
|
19865
|
+
/* A corner claim travels down (see group.jsx) because the member joined to
|
|
19866
|
+
its neighbours is not always the thing that draws the frame: it can be a
|
|
19867
|
+
bare wrapper, a tooltip, a link, and the control inside is what has to
|
|
19868
|
+
square. That only holds while the wrapper adds nothing of its own. A box
|
|
19869
|
+
that paints a background or a border, or that insets what it holds, IS the
|
|
19870
|
+
frame at that spot — what is inside it sits on padding or on that
|
|
19871
|
+
background, never on the corner the group squared — so the claim stops
|
|
19872
|
+
here, exactly as a control stops it once it has answered. */
|
|
19873
|
+
[navi-box-frame] > * {
|
|
19874
|
+
--x-corner-top-left-radius: initial;
|
|
19875
|
+
--x-corner-top-right-radius: initial;
|
|
19876
|
+
--x-corner-bottom-right-radius: initial;
|
|
19877
|
+
--x-corner-bottom-left-radius: initial;
|
|
19878
|
+
}
|
|
19879
|
+
|
|
19729
19880
|
@layer navi {
|
|
19730
19881
|
/*
|
|
19731
19882
|
When using square/circle/aspectRatio prop we expect box to respect the aspect ratio.
|
|
@@ -20012,10 +20163,27 @@ const computeBox = (props, parentBoxFlow) => {
|
|
|
20012
20163
|
rest["data-body"] = "";
|
|
20013
20164
|
// Padding is what decides whether the content reaches the body's own
|
|
20014
20165
|
// corners — see the corner claims in this file's CSS.
|
|
20015
|
-
|
|
20166
|
+
let flush = true;
|
|
20167
|
+
for (const name of PADDING_PROP_SET) {
|
|
20168
|
+
if (declaresSomething(rest[name])) {
|
|
20169
|
+
flush = false;
|
|
20170
|
+
break;
|
|
20171
|
+
}
|
|
20172
|
+
}
|
|
20173
|
+
if (flush) {
|
|
20016
20174
|
rest["data-body-flush"] = "";
|
|
20017
20175
|
}
|
|
20018
20176
|
}
|
|
20177
|
+
// A box that paints something of its own, or that insets what it holds, is
|
|
20178
|
+
// the frame at that spot: the corner claims coming from a Group are about
|
|
20179
|
+
// ITS corners and nothing inside reaches them, so they stop here — see this
|
|
20180
|
+
// file's CSS.
|
|
20181
|
+
for (const name of FRAME_PROP_SET) {
|
|
20182
|
+
if (declaresSomething(rest[name])) {
|
|
20183
|
+
rest["navi-box-frame"] = "";
|
|
20184
|
+
break;
|
|
20185
|
+
}
|
|
20186
|
+
}
|
|
20019
20187
|
const defaultDisplay = getDefaultDisplay(TagName);
|
|
20020
20188
|
let {
|
|
20021
20189
|
inline,
|
|
@@ -20446,8 +20614,14 @@ const shouldInjectSeparatorBetween = (left, right) => {
|
|
|
20446
20614
|
}
|
|
20447
20615
|
return true;
|
|
20448
20616
|
};
|
|
20449
|
-
const
|
|
20450
|
-
|
|
20617
|
+
const PADDING_PROP_SET = new Set(["padding", "paddingX", "paddingY", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft"]);
|
|
20618
|
+
/* What makes a box the frame at its spot: what it paints of its own, and the
|
|
20619
|
+
padding holding its content away from its corners. Deliberately without the
|
|
20620
|
+
radius props: a radius alone paints nothing — it only says how a background
|
|
20621
|
+
or a border already there is cut — so a box carrying just a radius is still
|
|
20622
|
+
a wrapper around whatever draws. */
|
|
20623
|
+
const FRAME_PROP_SET = new Set(["background", "backgroundColor", "backgroundImage", "border", "borderTop", "borderRight", "borderBottom", "borderLeft", "borderWidth", "borderColor", "borderStyle", ...PADDING_PROP_SET]);
|
|
20624
|
+
const declaresSomething = value => {
|
|
20451
20625
|
if (value === undefined || value === null || value === false) {
|
|
20452
20626
|
return false;
|
|
20453
20627
|
}
|
|
@@ -30042,6 +30216,13 @@ registerNaviCommand("--navi-unselect", (source, event) => {
|
|
|
30042
30216
|
* observeAncestorOpenState for exactly how that's detected, and why it
|
|
30043
30217
|
* matters that it happens before the browser paints.
|
|
30044
30218
|
* - Inside an open ancestor → runs on mount AND every subsequent open.
|
|
30219
|
+
* - Inside the always-on-screen part of a *closed* one — a picker's façade,
|
|
30220
|
+
* an expandable's header, a <summary>: those elements are displayed the
|
|
30221
|
+
* whole time their ancestor reads as closed (aria-expanded on a trigger
|
|
30222
|
+
* describes the popup it controls, not its own contents). They run on
|
|
30223
|
+
* mount like anything else on screen, and the ancestor opening later
|
|
30224
|
+
* reveals nothing about them, so it does not re-run them either. See
|
|
30225
|
+
* isDisplayedDespiteClosedAncestor in @jsenv/dom.
|
|
30045
30226
|
*
|
|
30046
30227
|
* The callback's second argument is always a `navi_displayed` CustomEvent,
|
|
30047
30228
|
* with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
|
|
@@ -30074,6 +30255,10 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
30074
30255
|
const callbackRef = useRef(callback);
|
|
30075
30256
|
callbackRef.current = callback;
|
|
30076
30257
|
|
|
30258
|
+
// Set by the mount effect below for an element that lives in its openable
|
|
30259
|
+
// ancestor's façade rather than in what that ancestor opens.
|
|
30260
|
+
const displayedWhileAncestorClosedRef = useRef(false);
|
|
30261
|
+
|
|
30077
30262
|
// Run on mount (or when deps change) — but only if the element is visible.
|
|
30078
30263
|
useLayoutEffect(() => {
|
|
30079
30264
|
const el = ref.current;
|
|
@@ -30086,9 +30271,16 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
30086
30271
|
return;
|
|
30087
30272
|
}
|
|
30088
30273
|
if (!isAncestorOpen(ancestor)) {
|
|
30089
|
-
|
|
30090
|
-
|
|
30091
|
-
|
|
30274
|
+
if (!isDisplayedDespiteClosedAncestor(el)) {
|
|
30275
|
+
// Ancestor is closed and took this element off screen with it — skip
|
|
30276
|
+
// now; the observeAncestorOpenState call below will fire once it
|
|
30277
|
+
// opens.
|
|
30278
|
+
return;
|
|
30279
|
+
}
|
|
30280
|
+
// Closed, yet on screen: the ancestor is the trigger of what is
|
|
30281
|
+
// closed, not the thing itself, and this element belongs to the façade
|
|
30282
|
+
// it keeps showing.
|
|
30283
|
+
displayedWhileAncestorClosedRef.current = true;
|
|
30092
30284
|
}
|
|
30093
30285
|
callbackRef.current(el, createDisplayedEvent(ancestor, false));
|
|
30094
30286
|
}, deps);
|
|
@@ -30107,6 +30299,12 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
30107
30299
|
if (!isOpen) {
|
|
30108
30300
|
return;
|
|
30109
30301
|
}
|
|
30302
|
+
if (displayedWhileAncestorClosedRef.current) {
|
|
30303
|
+
// Façade content: on screen the whole time, so this opening reveals
|
|
30304
|
+
// nothing here — and `becauseAncestorOpened: true` about it would be
|
|
30305
|
+
// false in a way consumers act on (see use_auto_focus.js).
|
|
30306
|
+
return;
|
|
30307
|
+
}
|
|
30110
30308
|
const lastEl = ref.current;
|
|
30111
30309
|
callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
|
|
30112
30310
|
});
|
|
@@ -30966,11 +31164,22 @@ const isTypingIntent = (e) =>
|
|
|
30966
31164
|
|
|
30967
31165
|
const s = (n) => (n > 1 ? "s" : "");
|
|
30968
31166
|
|
|
31167
|
+
// The `u` flag is what lets a char class speak about characters: `\p{...}` is
|
|
31168
|
+
// only recognized under it, and a range covers whole code points instead of
|
|
31169
|
+
// the two halves an astral character (an emoji) is made of.
|
|
31170
|
+
const compileCharClass = (charClass) => new RegExp(charClass, "u");
|
|
31171
|
+
const compileCharClassAnchored = (charClass) =>
|
|
31172
|
+
new RegExp(`^(?:${charClass})*$`, "u");
|
|
31173
|
+
|
|
30969
31174
|
// Keydown: block only single printable characters that don't match the class.
|
|
30970
31175
|
// Multi-character key names (Delete, ArrowLeft…) are always allowed.
|
|
30971
31176
|
const getInvalidCharMessage = (char, { charClass, messageKey }) => {
|
|
30972
|
-
|
|
30973
|
-
|
|
31177
|
+
// Counted in code points: an astral character is one character typed, not two.
|
|
31178
|
+
const codePointCount = [...char].length;
|
|
31179
|
+
if (codePointCount !== 1) {
|
|
31180
|
+
return null;
|
|
31181
|
+
}
|
|
31182
|
+
if (compileCharClass(charClass).test(char)) return null;
|
|
30974
31183
|
return naviI18n(messageKey);
|
|
30975
31184
|
};
|
|
30976
31185
|
|
|
@@ -30990,7 +31199,7 @@ const getMaxLengthInsertionMessage = (el, { maxLength }) => {
|
|
|
30990
31199
|
// Paste / set: block when value contains disallowed chars.
|
|
30991
31200
|
const getInvalidCharsMessage = (uiState, { charClass, messageKey }) => {
|
|
30992
31201
|
const str = uiState === undefined ? "" : String(uiState);
|
|
30993
|
-
if (
|
|
31202
|
+
if (compileCharClassAnchored(charClass).test(str)) return null;
|
|
30994
31203
|
return naviI18n(messageKey);
|
|
30995
31204
|
};
|
|
30996
31205
|
|
|
@@ -37217,6 +37426,7 @@ const TextAnchor = ({
|
|
|
37217
37426
|
ref: anchorRef,
|
|
37218
37427
|
className: "navi_text_anchor",
|
|
37219
37428
|
"aria-hidden": "true",
|
|
37429
|
+
hidden: true,
|
|
37220
37430
|
children: "\u200B"
|
|
37221
37431
|
})]
|
|
37222
37432
|
});
|
|
@@ -37884,8 +38094,10 @@ const shouldInjectSpacingBetween = (left, right) => {
|
|
|
37884
38094
|
* @param {boolean} [emojiAsIcon]
|
|
37885
38095
|
* Renders every emoji found in the string children as an `Icon`, so it sits
|
|
37886
38096
|
* 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
|
-
*
|
|
38097
|
+
* text. For free text a user typed (a message, a description); see
|
|
38098
|
+
* `docs/typography.md`. Only the strings this `Text` receives are rewritten —
|
|
38099
|
+
* a string a child component renders is out of reach, and that component
|
|
38100
|
+
* calls `renderEmojiAsIcon()` itself instead.
|
|
37889
38101
|
*
|
|
37890
38102
|
* @param {boolean} [preventSpaceUnderlines]
|
|
37891
38103
|
* Replaces real space characters between children with padding-based spaces.
|
|
@@ -46380,7 +46592,9 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
46380
46592
|
* "postal" → postal code (digits, letters, space, hyphen)
|
|
46381
46593
|
* "iban" → IBAN (uppercase and digits)
|
|
46382
46594
|
* "slug" → URL slug (lowercase, digits, hyphens)
|
|
46383
|
-
* "[A-Z0-9]" → any custom regex character class
|
|
46595
|
+
* "[A-Z0-9]" → any custom regex character class, compiled with the `u`
|
|
46596
|
+
* flag: `\p{...}` is available, and an emoji counts as one
|
|
46597
|
+
* character rather than two halves.
|
|
46384
46598
|
* inputMode and pattern are auto-derived from the preset when not explicitly set.
|
|
46385
46599
|
*
|
|
46386
46600
|
* - maxLengthGuard — combines maxLength + overflow guard in one prop.
|
|
@@ -59043,10 +59257,11 @@ const PendingScrollRefContext = createContext(null);
|
|
|
59043
59257
|
// "muted" — keep in DOM, visible but opacified and still interactive
|
|
59044
59258
|
const SearchNoMatchModeContext = createContext("remove");
|
|
59045
59259
|
|
|
59046
|
-
//
|
|
59047
|
-
//
|
|
59048
|
-
//
|
|
59049
|
-
//
|
|
59260
|
+
// How many rows the list draws at once. Past that, a render window [start, end)
|
|
59261
|
+
// caps the number of DOM nodes: the window slides as the user scrolls, using
|
|
59262
|
+
// actual DOM positions (getBoundingClientRect) to find the first visible item —
|
|
59263
|
+
// no height estimation. It frames the rows a run draws (see ListItems); items
|
|
59264
|
+
// declared one by one (<List.Item>) are all drawn, whatever the budget.
|
|
59050
59265
|
const RENDER_BUDGET_DEFAULT = 100;
|
|
59051
59266
|
|
|
59052
59267
|
// Attribute used on <li> elements rendered by ListItemReal so the scroll listener
|
|
@@ -59055,8 +59270,9 @@ const REAL_LIST_ITEM_SELECTOR = `[navi-list-item-real]`;
|
|
|
59055
59270
|
// Rows standing in for content that has not arrived (see List's renderSkeleton).
|
|
59056
59271
|
const SKELETON_LIST_ITEM_CLASS = "navi_list_item_skeleton";
|
|
59057
59272
|
|
|
59058
|
-
// Carries the render window {start, end}
|
|
59059
|
-
//
|
|
59273
|
+
// Carries the render window {start, end} from List down to the runs of rows
|
|
59274
|
+
// inside it (see ListItems): a run draws the rows it frames and holds the room
|
|
59275
|
+
// of the others.
|
|
59060
59276
|
const RenderWindowContext = createContext(null);
|
|
59061
59277
|
// Carries List's own `columns` prop (a grid-template-columns value, e.g.
|
|
59062
59278
|
// "1fr auto auto") down to each ListItem/filler/fallback so they can render
|
|
@@ -59893,12 +60109,46 @@ const ListUI = props => {
|
|
|
59893
60109
|
searchText,
|
|
59894
60110
|
horizontal
|
|
59895
60111
|
});
|
|
60112
|
+
|
|
60113
|
+
// renderBudget frames the rows of a run; a list whose items are all declared
|
|
60114
|
+
// one by one draws every one of them, and the prop looks exactly like it is
|
|
60115
|
+
// doing something. Said once per list, when there is something drawn to
|
|
60116
|
+
// judge it on — a run mounting later (rows behind a loading state) is not a
|
|
60117
|
+
// list without one.
|
|
60118
|
+
const renderBudgetWarnedRef = useRef(false);
|
|
60119
|
+
useLayoutEffect(() => {
|
|
60120
|
+
if (props.renderBudget === undefined || renderBudgetWarnedRef.current) {
|
|
60121
|
+
return;
|
|
60122
|
+
}
|
|
60123
|
+
if (virtual.hasRuns() || tracker.itemsSignal.peek().length === 0) {
|
|
60124
|
+
return;
|
|
60125
|
+
}
|
|
60126
|
+
renderBudgetWarnedRef.current = true;
|
|
60127
|
+
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.`);
|
|
60128
|
+
});
|
|
59896
60129
|
virtual.captureAnchor = captureAnchor;
|
|
59897
60130
|
virtual.virtualItemSizeSignal = virtualItemSizeSignal;
|
|
59898
60131
|
virtual.horizontal = Boolean(horizontal);
|
|
59899
60132
|
virtual.renderSkeleton = renderSkeleton;
|
|
60133
|
+
|
|
60134
|
+
// A row is addressed by id from outside (--navi-scroll, --navi-select): the
|
|
60135
|
+
// ones drawn have registered themselves with the tracker, and the ones a run
|
|
60136
|
+
// holds without drawing are known only to that run (see List.Items' row
|
|
60137
|
+
// locator). Both answer here, so a row is reachable whether or not the
|
|
60138
|
+
// window happens to frame it.
|
|
59900
60139
|
const getItemById = itemId => {
|
|
59901
|
-
|
|
60140
|
+
const itemDrawn = tracker.itemsSignal.peek().find(item => item.id === itemId);
|
|
60141
|
+
if (itemDrawn) {
|
|
60142
|
+
return itemDrawn;
|
|
60143
|
+
}
|
|
60144
|
+
const rowIndex = virtual.locateRow(itemId);
|
|
60145
|
+
if (rowIndex === null) {
|
|
60146
|
+
return undefined;
|
|
60147
|
+
}
|
|
60148
|
+
return {
|
|
60149
|
+
id: itemId,
|
|
60150
|
+
index: rowIndex
|
|
60151
|
+
};
|
|
59902
60152
|
};
|
|
59903
60153
|
const noMatchCount = tracker.noMatchCountSignal.value;
|
|
59904
60154
|
// What the list stands for, which is not always what it holds: a run saying
|
|
@@ -62438,6 +62688,9 @@ const createListVirtual = () => {
|
|
|
62438
62688
|
passId++;
|
|
62439
62689
|
nextIndex = 0;
|
|
62440
62690
|
},
|
|
62691
|
+
// Whether any run of rows lives in this list: what makes a render window
|
|
62692
|
+
// mean anything (see List's renderBudget).
|
|
62693
|
+
hasRuns: () => locatorByOwner.size > 0,
|
|
62441
62694
|
setRowLocator: (ownerId, locate) => {
|
|
62442
62695
|
locatorByOwner.set(ownerId, locate);
|
|
62443
62696
|
},
|
|
@@ -62484,12 +62737,17 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62484
62737
|
* for them — which is what makes an infinitely scrolled list nothing more than
|
|
62485
62738
|
* a list that says how many rows it has.
|
|
62486
62739
|
*
|
|
62487
|
-
*
|
|
62488
|
-
*
|
|
62489
|
-
*
|
|
62490
|
-
*
|
|
62491
|
-
*
|
|
62492
|
-
*
|
|
62740
|
+
* A collection held in memory is given whole: `items={rows}`. The run holds all
|
|
62741
|
+
* of them from the first render and never asks for anything — it is still the
|
|
62742
|
+
* render window that decides how many are drawn.
|
|
62743
|
+
*
|
|
62744
|
+
* A collection read a slice at a time comes from `itemsAction(range)`: the run
|
|
62745
|
+
* asks for what it is about to draw and keeps what it gets. The range says the
|
|
62746
|
+
* same thing three ways, so a source can read it however it paginates —
|
|
62747
|
+
* `{ start, end }` (places in the collection, a negative `start` counting back
|
|
62748
|
+
* from the end like `Range: items=-25`, which is what a list opening on its
|
|
62749
|
+
* last rows asks for before it knows how many there are), `limit` (how many
|
|
62750
|
+
* rows), and
|
|
62493
62751
|
* `before`/`after`/`around` (the id of a row to count from, for a source
|
|
62494
62752
|
* paginating by cursor). Answer with the rows (an array — that is all of
|
|
62495
62753
|
* them), or with a range the way a Content-Range does: `{ items, start, count }`
|
|
@@ -62503,11 +62761,11 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62503
62761
|
* copies of the JSON. The list holds the slices, the store holds the objects
|
|
62504
62762
|
* (see docs/resource.md).
|
|
62505
62763
|
*
|
|
62506
|
-
* A collection
|
|
62507
|
-
*
|
|
62508
|
-
*
|
|
62509
|
-
* with it, the way one does for anything else that is not the same
|
|
62510
|
-
* anymore.
|
|
62764
|
+
* A collection that changes as a whole (a search reordering it) is a different
|
|
62765
|
+
* collection: with `items`, another array is another collection and the run
|
|
62766
|
+
* draws it from its first row; with `itemsAction`, give the run a `key` that
|
|
62767
|
+
* changes with it, the way one does for anything else that is not the same
|
|
62768
|
+
* thing anymore.
|
|
62511
62769
|
*
|
|
62512
62770
|
* A row says what it is where it is drawn: `renderItem` returns a
|
|
62513
62771
|
* `<List.Item>` carrying its own props (`selectable`, `value`, `selected`…),
|
|
@@ -62519,7 +62777,8 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62519
62777
|
*
|
|
62520
62778
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
62521
62779
|
* renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
|
|
62522
|
-
*
|
|
62780
|
+
* items?: any[],
|
|
62781
|
+
* itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
|
|
62523
62782
|
* count?: number,
|
|
62524
62783
|
* groupBy?: (item: any, index: number) => any,
|
|
62525
62784
|
* renderGroupLabel?: (item: any, index: number) => import("ignore:preact").ComponentChildren,
|
|
@@ -62534,6 +62793,13 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62534
62793
|
* What one row is, given the item and where it sits. `state.refreshing` says
|
|
62535
62794
|
* the rows drawn are the ones from before while the run reads the collection
|
|
62536
62795
|
* again — the list carries `navi-refreshing` for the same reason.
|
|
62796
|
+
* @param {any[]} [props.items]
|
|
62797
|
+
* The collection, when it is held in memory: all of it, in order. Nothing is
|
|
62798
|
+
* ever asked for — `itemsAction`, `count`, `pageSize` and `memoryBudget` have
|
|
62799
|
+
* no part to play, and no row is ever a skeleton.
|
|
62800
|
+
* @param {(range: object) => any} [props.itemsAction]
|
|
62801
|
+
* Where the rows come from when the collection is read a slice at a time:
|
|
62802
|
+
* a resource's range reader (`RESOURCE.GET_RANGE.bindParams(...)`).
|
|
62537
62803
|
* @param {(item: any, index: number) => any} [props.groupBy]
|
|
62538
62804
|
* What tells rows that belong together apart from the others — the day of a
|
|
62539
62805
|
* message, the month of a game. Consecutive rows sharing it are wrapped in a
|
|
@@ -62556,7 +62822,8 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62556
62822
|
* How many rows the run keeps in memory. Past that, the ones far from what is
|
|
62557
62823
|
* on screen are dropped (and asked for again if the user goes back) — the
|
|
62558
62824
|
* same trade the render window makes with the DOM, one order of magnitude
|
|
62559
|
-
* further out. `0` keeps
|
|
62825
|
+
* further out. `Infinity` keeps every row the run ever received; `0` keeps
|
|
62826
|
+
* only the ones around the window.
|
|
62560
62827
|
* @param {false|(index: number) => any} [props.renderSkeleton]
|
|
62561
62828
|
* What to draw for a row the run does not hold. Defaults to List's own
|
|
62562
62829
|
* `renderSkeleton`, then to a bare `<List.Item skeleton>`; `false` leaves the
|
|
@@ -62577,6 +62844,7 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62577
62844
|
*/
|
|
62578
62845
|
const ListItems = ({
|
|
62579
62846
|
renderItem,
|
|
62847
|
+
items,
|
|
62580
62848
|
itemsAction,
|
|
62581
62849
|
count,
|
|
62582
62850
|
pageSize,
|
|
@@ -62593,6 +62861,7 @@ const ListItems = ({
|
|
|
62593
62861
|
const renderWindow = useContext(RenderWindowContext);
|
|
62594
62862
|
const separator = useContext(SeparatorContext);
|
|
62595
62863
|
const store = useItemStore({
|
|
62864
|
+
items,
|
|
62596
62865
|
count,
|
|
62597
62866
|
itemsAction,
|
|
62598
62867
|
memoryBudget,
|
|
@@ -62883,11 +63152,17 @@ const rangeIsSame = (a, b) => {
|
|
|
62883
63152
|
// are in all is enough to place it, so the pages need not be contiguous nor
|
|
62884
63153
|
// arrive in order.
|
|
62885
63154
|
const useItemStore = ({
|
|
63155
|
+
items,
|
|
62886
63156
|
count,
|
|
62887
63157
|
itemsAction,
|
|
62888
63158
|
memoryBudget,
|
|
62889
63159
|
onRequestStateChange
|
|
62890
63160
|
}) => {
|
|
63161
|
+
// A collection given whole (`items`) is held from the first render: nothing
|
|
63162
|
+
// to ask for, nothing to keep across mounts, nothing to invalidate. The rest
|
|
63163
|
+
// of the store then never has a hole to fill, so it stays inert on its own —
|
|
63164
|
+
// the asking below finds nothing missing.
|
|
63165
|
+
const inMemory = items !== undefined;
|
|
62891
63166
|
// The run's asking, on the same channel as the window it asks for — they are
|
|
62892
63167
|
// one subject: what the list is about to draw is what it goes to fetch (see
|
|
62893
63168
|
// `useRequestMissing`, and `updateRenderWindow` which logs the other half).
|
|
@@ -62909,8 +63184,28 @@ const useItemStore = ({
|
|
|
62909
63184
|
// the reader holds for it (see resource_range_reader.js).
|
|
62910
63185
|
useRef(false);
|
|
62911
63186
|
const pagesRef = useRef(null);
|
|
63187
|
+
const itemsRef = useRef(items);
|
|
63188
|
+
const itemsHeldRef = useRef(false);
|
|
62912
63189
|
let restored = false;
|
|
62913
|
-
if (
|
|
63190
|
+
if (inMemory) {
|
|
63191
|
+
// The array as a whole is the collection: another array is another
|
|
63192
|
+
// collection, drawn from its first row (which is also why a run reading a
|
|
63193
|
+
// collection that changes as a whole takes a key).
|
|
63194
|
+
if (!pagesRef.current || itemsRef.current !== items) {
|
|
63195
|
+
itemsRef.current = items;
|
|
63196
|
+
const byIndex = new Map();
|
|
63197
|
+
let index = 0;
|
|
63198
|
+
while (index < items.length) {
|
|
63199
|
+
byIndex.set(index, items[index]);
|
|
63200
|
+
index++;
|
|
63201
|
+
}
|
|
63202
|
+
pagesRef.current = {
|
|
63203
|
+
byIndex,
|
|
63204
|
+
count: items.length
|
|
63205
|
+
};
|
|
63206
|
+
itemsHeldRef.current = false;
|
|
63207
|
+
}
|
|
63208
|
+
} else if (!pagesRef.current) {
|
|
62914
63209
|
const composition = typeof itemsAction === "function" && itemsAction.readComposition ? itemsAction.readComposition() : null;
|
|
62915
63210
|
if (composition && composition.count !== undefined) {
|
|
62916
63211
|
pagesRef.current = composition;
|
|
@@ -62957,6 +63252,16 @@ const useItemStore = ({
|
|
|
62957
63252
|
// range askable again (see the request memory just above).
|
|
62958
63253
|
const [failure, setFailure] = useState(null);
|
|
62959
63254
|
const virtual = useContext(ListVirtualContext);
|
|
63255
|
+
// The rows are there, which is what the list waits for to place itself on the
|
|
63256
|
+
// row it is held at (see placeWhereHeld). Said from an effect: a signal read
|
|
63257
|
+
// during this very render must not be written during it.
|
|
63258
|
+
useLayoutEffect(() => {
|
|
63259
|
+
if (!inMemory || itemsHeldRef.current) {
|
|
63260
|
+
return;
|
|
63261
|
+
}
|
|
63262
|
+
itemsHeldRef.current = true;
|
|
63263
|
+
virtual.pagesSignal.value = virtual.pagesSignal.peek() + 1;
|
|
63264
|
+
});
|
|
62960
63265
|
// Before the first answer a run does not know how many rows it stands for.
|
|
62961
63266
|
// It stands for a windowful of them: a list that is about to be filled looks
|
|
62962
63267
|
// like rows on their way, not like an empty list.
|
|
@@ -63029,10 +63334,12 @@ const useItemStore = ({
|
|
|
63029
63334
|
// dropped and simply asked for again if the user goes back — the same
|
|
63030
63335
|
// trade the render window makes, one order of magnitude further out.
|
|
63031
63336
|
forget: (windowFrom, windowTo) => {
|
|
63032
|
-
|
|
63033
|
-
|
|
63337
|
+
if (inMemory) {
|
|
63338
|
+
// Dropping a row here would drop it for good: there is no source to
|
|
63339
|
+
// ask it back from.
|
|
63034
63340
|
return;
|
|
63035
63341
|
}
|
|
63342
|
+
const budget = memoryBudget === undefined ? ITEM_STORE_MAX_DEFAULT : memoryBudget;
|
|
63036
63343
|
const keepFrom = windowFrom - ITEM_STORE_KEEP_AROUND;
|
|
63037
63344
|
const keepTo = windowTo + ITEM_STORE_KEEP_AROUND;
|
|
63038
63345
|
if (pages.byIndex.size > budget) {
|