@jsenv/navi 0.29.108 → 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 +502 -130
- package/dist/jsenv_navi.js.map +45 -37
- 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 +10 -1
- 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,
|
|
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
|
}
|
|
@@ -25330,75 +25504,85 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
25330
25504
|
return requestedResult;
|
|
25331
25505
|
};
|
|
25332
25506
|
|
|
25333
|
-
//
|
|
25507
|
+
// A click on a link is answered on the link itself, after the link's own
|
|
25508
|
+
// listeners: a navi control decides there whether the press is allowed — it
|
|
25509
|
+
// refuses with preventDefault from its click reaction — and runs its command
|
|
25510
|
+
// and its action; only then does the routing take the browser's place. This
|
|
25511
|
+
// is the order the Navigation API integration gets for free, where the
|
|
25512
|
+
// navigation starts once the click has been dispatched.
|
|
25513
|
+
const onLinkClick = (e) => {
|
|
25514
|
+
if (e.button !== 0) {
|
|
25515
|
+
// Ignore non-left clicks
|
|
25516
|
+
return;
|
|
25517
|
+
}
|
|
25518
|
+
if (e.metaKey) {
|
|
25519
|
+
// Ignore clicks with meta key (e.g. open in new tab)
|
|
25520
|
+
return;
|
|
25521
|
+
}
|
|
25522
|
+
if (e.defaultPrevented) {
|
|
25523
|
+
// Refused — by the link itself, or by whoever came before it.
|
|
25524
|
+
return;
|
|
25525
|
+
}
|
|
25526
|
+
const linkElement = e.currentTarget;
|
|
25527
|
+
if (linkElement.hasAttribute("data-readonly")) {
|
|
25528
|
+
return;
|
|
25529
|
+
}
|
|
25530
|
+
const href = linkElement.href;
|
|
25531
|
+
const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
|
|
25532
|
+
getHrefTargetInfo(href);
|
|
25533
|
+
if (isEmpty || !isSameOrigin) {
|
|
25534
|
+
// Let link to other origins be handled by the browser
|
|
25535
|
+
return;
|
|
25536
|
+
}
|
|
25537
|
+
if (isAnchor) {
|
|
25538
|
+
// Fragment navigation belongs to the browser: it owns the indicated
|
|
25539
|
+
// part of the document, and taking it over would cost `:target` and the
|
|
25540
|
+
// focus handling that come with it.
|
|
25541
|
+
if (isCurrent) {
|
|
25542
|
+
// Except this one, which the browser answers with a scroll and
|
|
25543
|
+
// nothing else: same pathname, same hash, so no event and no url
|
|
25544
|
+
// change reaches whoever is waiting on the designated element.
|
|
25545
|
+
rearmUrlTarget();
|
|
25546
|
+
}
|
|
25547
|
+
return;
|
|
25548
|
+
}
|
|
25549
|
+
// Nothing here declared a route, so there is nothing to route to: the
|
|
25550
|
+
// page is a plain document and a link in it is a plain link. Taking it
|
|
25551
|
+
// over anyway would push the url and then have nothing to show for it —
|
|
25552
|
+
// the address bar moves and the page does not (see applyRouting's own
|
|
25553
|
+
// "not called yet" branch, which is where that used to end up).
|
|
25554
|
+
if (!isRouting()) {
|
|
25555
|
+
return;
|
|
25556
|
+
}
|
|
25557
|
+
e.preventDefault();
|
|
25558
|
+
handleRoutingTask(href, {
|
|
25559
|
+
reason: `"click" on a[href="${href}"]`,
|
|
25560
|
+
// A link that takes the place of the current entry instead of stacking
|
|
25561
|
+
// on it says so on itself (see link_replace.js).
|
|
25562
|
+
navigationType: linkAsksForReplace(linkElement) ? "replace" : "push",
|
|
25563
|
+
// Who started it. Announced with the navigation because a press
|
|
25564
|
+
// carries things the url does not: what a link asks of a route
|
|
25565
|
+
// transition is the first of them (see route_transition.jsx). Read by
|
|
25566
|
+
// whoever knows what to do with it, and it is the anchor itself —
|
|
25567
|
+
// resolved here, where it already is.
|
|
25568
|
+
element: linkElement,
|
|
25569
|
+
});
|
|
25570
|
+
};
|
|
25571
|
+
// Wired from the capture phase on window, the first place a click is seen,
|
|
25572
|
+
// so that a link is answered whatever handler stops the click on its way up
|
|
25573
|
+
// (a card's own onClick). Wired on every click: a listener an element
|
|
25574
|
+
// already has is not added twice, and a link seen for the first time is
|
|
25575
|
+
// wired before the click reaches it. The click a gesture leaves behind
|
|
25576
|
+
// never gets this far — its suppressor (click_suppression.js in
|
|
25577
|
+
// @jsenv/dom) stops it on window, before anything below.
|
|
25334
25578
|
window.addEventListener(
|
|
25335
25579
|
"click",
|
|
25336
25580
|
(e) => {
|
|
25337
|
-
if (e.button !== 0) {
|
|
25338
|
-
// Ignore non-left clicks
|
|
25339
|
-
return;
|
|
25340
|
-
}
|
|
25341
|
-
if (e.metaKey) {
|
|
25342
|
-
// Ignore clicks with meta key (e.g. open in new tab)
|
|
25343
|
-
return;
|
|
25344
|
-
}
|
|
25345
|
-
if (e.defaultPrevented) {
|
|
25346
|
-
return;
|
|
25347
|
-
}
|
|
25348
|
-
if (clickIsSuppressed()) {
|
|
25349
|
-
// The click that ends a gesture (click_suppression.js in @jsenv/dom).
|
|
25350
|
-
// Its suppressor also listens on window in capture, so whichever
|
|
25351
|
-
// module registered first runs first — asked explicitly, the order
|
|
25352
|
-
// stops mattering.
|
|
25353
|
-
return;
|
|
25354
|
-
}
|
|
25355
25581
|
const linkElement = e.target.closest("a");
|
|
25356
25582
|
if (!linkElement) {
|
|
25357
25583
|
return;
|
|
25358
25584
|
}
|
|
25359
|
-
|
|
25360
|
-
return;
|
|
25361
|
-
}
|
|
25362
|
-
const href = linkElement.href;
|
|
25363
|
-
const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
|
|
25364
|
-
getHrefTargetInfo(href);
|
|
25365
|
-
if (isEmpty || !isSameOrigin) {
|
|
25366
|
-
// Let link to other origins be handled by the browser
|
|
25367
|
-
return;
|
|
25368
|
-
}
|
|
25369
|
-
if (isAnchor) {
|
|
25370
|
-
// Fragment navigation belongs to the browser: it owns the indicated
|
|
25371
|
-
// part of the document, and taking it over would cost `:target` and the
|
|
25372
|
-
// focus handling that come with it.
|
|
25373
|
-
if (isCurrent) {
|
|
25374
|
-
// Except this one, which the browser answers with a scroll and
|
|
25375
|
-
// nothing else: same pathname, same hash, so no event and no url
|
|
25376
|
-
// change reaches whoever is waiting on the designated element.
|
|
25377
|
-
rearmUrlTarget();
|
|
25378
|
-
}
|
|
25379
|
-
return;
|
|
25380
|
-
}
|
|
25381
|
-
// Nothing here declared a route, so there is nothing to route to: the
|
|
25382
|
-
// page is a plain document and a link in it is a plain link. Taking it
|
|
25383
|
-
// over anyway would push the url and then have nothing to show for it —
|
|
25384
|
-
// the address bar moves and the page does not (see applyRouting's own
|
|
25385
|
-
// "not called yet" branch, which is where that used to end up).
|
|
25386
|
-
if (!isRouting()) {
|
|
25387
|
-
return;
|
|
25388
|
-
}
|
|
25389
|
-
e.preventDefault();
|
|
25390
|
-
handleRoutingTask(href, {
|
|
25391
|
-
reason: `"click" on a[href="${href}"]`,
|
|
25392
|
-
// A link that takes the place of the current entry instead of stacking
|
|
25393
|
-
// on it says so on itself (see link_replace.js).
|
|
25394
|
-
navigationType: linkAsksForReplace(linkElement) ? "replace" : "push",
|
|
25395
|
-
// Who started it. Announced with the navigation because a press
|
|
25396
|
-
// carries things the url does not: what a link asks of a route
|
|
25397
|
-
// transition is the first of them (see route_transition.jsx). Read by
|
|
25398
|
-
// whoever knows what to do with it, and it is the anchor itself —
|
|
25399
|
-
// resolved here, where it already is.
|
|
25400
|
-
element: linkElement,
|
|
25401
|
-
});
|
|
25585
|
+
linkElement.addEventListener("click", onLinkClick);
|
|
25402
25586
|
},
|
|
25403
25587
|
{ capture: true },
|
|
25404
25588
|
);
|
|
@@ -29838,7 +30022,11 @@ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
|
|
|
29838
30022
|
},
|
|
29839
30023
|
};
|
|
29840
30024
|
});
|
|
29841
|
-
|
|
30025
|
+
// "--navi-close:all" closes every expandable above the source, nearest first —
|
|
30026
|
+
// a link leaving from a badge shown over a sheet leaves both. A surface that
|
|
30027
|
+
// refuses (a form asking about its changes) keeps what is above it open too:
|
|
30028
|
+
// one cannot be out of the sheet while still in the badge.
|
|
30029
|
+
registerNaviCommand("--navi-close", (source, event, { argument }) => {
|
|
29842
30030
|
const target =
|
|
29843
30031
|
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
29844
30032
|
if (!target) {
|
|
@@ -29847,13 +30035,30 @@ registerNaviCommand("--navi-close", (source, event) => {
|
|
|
29847
30035
|
return {
|
|
29848
30036
|
target,
|
|
29849
30037
|
implementation: () => {
|
|
29850
|
-
|
|
29851
|
-
|
|
29852
|
-
|
|
29853
|
-
}
|
|
30038
|
+
const detail = { event, source: resolveCommandProxySource(source) };
|
|
30039
|
+
if (argument === "all") {
|
|
30040
|
+
return requestCloseUpward(target, detail);
|
|
30041
|
+
}
|
|
30042
|
+
return dispatchCustomEvent(target, "navi_request_close", detail);
|
|
29854
30043
|
},
|
|
29855
30044
|
};
|
|
29856
30045
|
});
|
|
30046
|
+
const requestCloseUpward = (target, detail) => {
|
|
30047
|
+
let expandable = target;
|
|
30048
|
+
while (expandable) {
|
|
30049
|
+
const closing = dispatchCustomEvent(
|
|
30050
|
+
expandable,
|
|
30051
|
+
"navi_request_close",
|
|
30052
|
+
detail,
|
|
30053
|
+
);
|
|
30054
|
+
if (!closing) {
|
|
30055
|
+
return false;
|
|
30056
|
+
}
|
|
30057
|
+
const parent = expandable.parentElement;
|
|
30058
|
+
expandable = parent ? parent.closest("[aria-expanded]") : null;
|
|
30059
|
+
}
|
|
30060
|
+
return true;
|
|
30061
|
+
};
|
|
29857
30062
|
registerNaviCommand("--navi-cancel", (source, event) => {
|
|
29858
30063
|
const target =
|
|
29859
30064
|
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
@@ -30011,6 +30216,13 @@ registerNaviCommand("--navi-unselect", (source, event) => {
|
|
|
30011
30216
|
* observeAncestorOpenState for exactly how that's detected, and why it
|
|
30012
30217
|
* matters that it happens before the browser paints.
|
|
30013
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.
|
|
30014
30226
|
*
|
|
30015
30227
|
* The callback's second argument is always a `navi_displayed` CustomEvent,
|
|
30016
30228
|
* with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
|
|
@@ -30043,6 +30255,10 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
30043
30255
|
const callbackRef = useRef(callback);
|
|
30044
30256
|
callbackRef.current = callback;
|
|
30045
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
|
+
|
|
30046
30262
|
// Run on mount (or when deps change) — but only if the element is visible.
|
|
30047
30263
|
useLayoutEffect(() => {
|
|
30048
30264
|
const el = ref.current;
|
|
@@ -30055,9 +30271,16 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
30055
30271
|
return;
|
|
30056
30272
|
}
|
|
30057
30273
|
if (!isAncestorOpen(ancestor)) {
|
|
30058
|
-
|
|
30059
|
-
|
|
30060
|
-
|
|
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;
|
|
30061
30284
|
}
|
|
30062
30285
|
callbackRef.current(el, createDisplayedEvent(ancestor, false));
|
|
30063
30286
|
}, deps);
|
|
@@ -30076,6 +30299,12 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
30076
30299
|
if (!isOpen) {
|
|
30077
30300
|
return;
|
|
30078
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
|
+
}
|
|
30079
30308
|
const lastEl = ref.current;
|
|
30080
30309
|
callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
|
|
30081
30310
|
});
|
|
@@ -30935,11 +31164,22 @@ const isTypingIntent = (e) =>
|
|
|
30935
31164
|
|
|
30936
31165
|
const s = (n) => (n > 1 ? "s" : "");
|
|
30937
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
|
+
|
|
30938
31174
|
// Keydown: block only single printable characters that don't match the class.
|
|
30939
31175
|
// Multi-character key names (Delete, ArrowLeft…) are always allowed.
|
|
30940
31176
|
const getInvalidCharMessage = (char, { charClass, messageKey }) => {
|
|
30941
|
-
|
|
30942
|
-
|
|
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;
|
|
30943
31183
|
return naviI18n(messageKey);
|
|
30944
31184
|
};
|
|
30945
31185
|
|
|
@@ -30959,7 +31199,7 @@ const getMaxLengthInsertionMessage = (el, { maxLength }) => {
|
|
|
30959
31199
|
// Paste / set: block when value contains disallowed chars.
|
|
30960
31200
|
const getInvalidCharsMessage = (uiState, { charClass, messageKey }) => {
|
|
30961
31201
|
const str = uiState === undefined ? "" : String(uiState);
|
|
30962
|
-
if (
|
|
31202
|
+
if (compileCharClassAnchored(charClass).test(str)) return null;
|
|
30963
31203
|
return naviI18n(messageKey);
|
|
30964
31204
|
};
|
|
30965
31205
|
|
|
@@ -33573,6 +33813,13 @@ const useControlProps = (props, {
|
|
|
33573
33813
|
}
|
|
33574
33814
|
};
|
|
33575
33815
|
}
|
|
33816
|
+
if (getKeyboardEventDefaultAction(e) === "activate") {
|
|
33817
|
+
// Enter: the browser presses the link itself, with a click that
|
|
33818
|
+
// follows this keydown — and the click reaction below is where
|
|
33819
|
+
// the press is answered, once. A tab for a slide (no href) gets
|
|
33820
|
+
// no such click; Link answers Enter on its own there.
|
|
33821
|
+
return null;
|
|
33822
|
+
}
|
|
33576
33823
|
return keyDownDefault(e);
|
|
33577
33824
|
},
|
|
33578
33825
|
click: e => {
|
|
@@ -37179,6 +37426,7 @@ const TextAnchor = ({
|
|
|
37179
37426
|
ref: anchorRef,
|
|
37180
37427
|
className: "navi_text_anchor",
|
|
37181
37428
|
"aria-hidden": "true",
|
|
37429
|
+
hidden: true,
|
|
37182
37430
|
children: "\u200B"
|
|
37183
37431
|
})]
|
|
37184
37432
|
});
|
|
@@ -37846,8 +38094,10 @@ const shouldInjectSpacingBetween = (left, right) => {
|
|
|
37846
38094
|
* @param {boolean} [emojiAsIcon]
|
|
37847
38095
|
* Renders every emoji found in the string children as an `Icon`, so it sits
|
|
37848
38096
|
* in the line like a character and never makes the line taller than the
|
|
37849
|
-
* text. For free text a user typed (a message, a description)
|
|
37850
|
-
*
|
|
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.
|
|
37851
38101
|
*
|
|
37852
38102
|
* @param {boolean} [preventSpaceUnderlines]
|
|
37853
38103
|
* Replaces real space characters between children with padding-based spaces.
|
|
@@ -39127,9 +39377,12 @@ const LinkPlain = props => {
|
|
|
39127
39377
|
replace: undefined,
|
|
39128
39378
|
"data-navi-route-transition-request": routeTransitionRequest,
|
|
39129
39379
|
...replaceRequest,
|
|
39380
|
+
// The control's own handlers first — the interaction gate, the caller's
|
|
39381
|
+
// onClick/onKeyDown, the command and the action — then what only a link
|
|
39382
|
+
// does. Written over the spread above, so they have to be called here.
|
|
39130
39383
|
onClick: e => {
|
|
39131
|
-
onClick
|
|
39132
|
-
if (slide) {
|
|
39384
|
+
controlHostProps.onClick(e);
|
|
39385
|
+
if (slide && !e.defaultPrevented) {
|
|
39133
39386
|
goToSlide(e.currentTarget, e);
|
|
39134
39387
|
}
|
|
39135
39388
|
if (preventDefault) {
|
|
@@ -39142,7 +39395,7 @@ const LinkPlain = props => {
|
|
|
39142
39395
|
,
|
|
39143
39396
|
|
|
39144
39397
|
onKeyDown: e => {
|
|
39145
|
-
|
|
39398
|
+
controlHostProps.onKeyDown(e);
|
|
39146
39399
|
if (!slide || e.defaultPrevented) {
|
|
39147
39400
|
return;
|
|
39148
39401
|
}
|
|
@@ -45772,6 +46025,9 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
45772
46025
|
* `--navi-nav-to` command — by TAKING THE PLACE of the current history entry
|
|
45773
46026
|
* rather than stacking on it: what `<Link replace>` says, for a press drawn
|
|
45774
46027
|
* as a button.
|
|
46028
|
+
* @param {Function} [action] On a button with an `href` or a `route`, the
|
|
46029
|
+
* same order as a Link's: it runs on the press, before the navigation, and
|
|
46030
|
+
* the navigation does not wait for it (see Link's `action`).
|
|
45775
46031
|
* @param {boolean} [emojiAsIcon=true] Renders the emoji of the label as icons
|
|
45776
46032
|
* so the button keeps the height of its text — `Text`'s prop, on by default
|
|
45777
46033
|
* here. Pass `false` to let an emoji draw at its natural size.
|
|
@@ -46336,7 +46592,9 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
46336
46592
|
* "postal" → postal code (digits, letters, space, hyphen)
|
|
46337
46593
|
* "iban" → IBAN (uppercase and digits)
|
|
46338
46594
|
* "slug" → URL slug (lowercase, digits, hyphens)
|
|
46339
|
-
* "[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.
|
|
46340
46598
|
* inputMode and pattern are auto-derived from the preset when not explicitly set.
|
|
46341
46599
|
*
|
|
46342
46600
|
* - maxLengthGuard — combines maxLength + overflow guard in one prop.
|
|
@@ -51781,7 +52039,8 @@ const FOCUS_DELAY_ON_KEYBOARD_MS = 250;
|
|
|
51781
52039
|
* - `open()`: requests opening — calls the caller's `onOpen` (see below), then
|
|
51782
52040
|
* `mountContent`/`openEffect`, then `openHandler`.
|
|
51783
52041
|
* - `requestClose()`: requests closing — calls `onRequestClose` then `onClose`,
|
|
51784
|
-
* stopping after the first if denied. The popup may choose to stay open
|
|
52042
|
+
* stopping after the first if denied. The popup may choose to stay open,
|
|
52043
|
+
* which is what a `false` return says (`true`: closed, or closed already).
|
|
51785
52044
|
* - `close()`: closes for real — calls only `onClose`, skipping
|
|
51786
52045
|
* `onRequestClose` entirely. Used when there really is no choice (e.g. the
|
|
51787
52046
|
* popup unmounting).
|
|
@@ -52074,7 +52333,7 @@ const createOpenController = (
|
|
|
52074
52333
|
detail,
|
|
52075
52334
|
) => {
|
|
52076
52335
|
if (!controller.opened) {
|
|
52077
|
-
return;
|
|
52336
|
+
return true;
|
|
52078
52337
|
}
|
|
52079
52338
|
const requestCloseEvent = new CustomEvent("navi_request_close", {
|
|
52080
52339
|
detail: { event: e, ...detail },
|
|
@@ -52089,9 +52348,10 @@ const createOpenController = (
|
|
|
52089
52348
|
if (nativeCancelEvent) {
|
|
52090
52349
|
nativeCancelEvent.preventDefault();
|
|
52091
52350
|
}
|
|
52092
|
-
return;
|
|
52351
|
+
return false;
|
|
52093
52352
|
}
|
|
52094
52353
|
performClose(requestCloseEvent);
|
|
52354
|
+
return true;
|
|
52095
52355
|
},
|
|
52096
52356
|
close: (e = new CustomEvent("programmatic", { detail: {} }), detail) => {
|
|
52097
52357
|
if (!controller.opened) {
|
|
@@ -53658,10 +53918,14 @@ const UncontrolledDialog = props => {
|
|
|
53658
53918
|
});
|
|
53659
53919
|
},
|
|
53660
53920
|
onnavi_request_close: e => {
|
|
53661
|
-
openController.requestClose(e, {
|
|
53921
|
+
const closing = openController.requestClose(e, {
|
|
53662
53922
|
isCancel: e.detail?.isCancel,
|
|
53663
53923
|
requester: e.detail?.source
|
|
53664
53924
|
});
|
|
53925
|
+
if (!closing) {
|
|
53926
|
+
// Said back to whoever asked: --navi-close:all stops climbing here.
|
|
53927
|
+
e.preventDefault();
|
|
53928
|
+
}
|
|
53665
53929
|
}
|
|
53666
53930
|
});
|
|
53667
53931
|
};
|
|
@@ -55108,9 +55372,13 @@ const UncontrolledPopover = props => {
|
|
|
55108
55372
|
});
|
|
55109
55373
|
},
|
|
55110
55374
|
onnavi_request_close: e => {
|
|
55111
|
-
openController.requestClose(e, {
|
|
55375
|
+
const closing = openController.requestClose(e, {
|
|
55112
55376
|
isCancel: e.detail?.isCancel
|
|
55113
55377
|
});
|
|
55378
|
+
if (!closing) {
|
|
55379
|
+
// Said back to whoever asked: --navi-close:all stops climbing here.
|
|
55380
|
+
e.preventDefault();
|
|
55381
|
+
}
|
|
55114
55382
|
}
|
|
55115
55383
|
});
|
|
55116
55384
|
};
|
|
@@ -56433,6 +56701,9 @@ const PickerCustomResolver = props => {
|
|
|
56433
56701
|
if (circle) {
|
|
56434
56702
|
props.variant = "icon";
|
|
56435
56703
|
}
|
|
56704
|
+
// A door, never a field (see `allowNameless`): the form around it expects
|
|
56705
|
+
// no value from it, and no name.
|
|
56706
|
+
props.allowNameless = true;
|
|
56436
56707
|
// A word in a sentence asks for a plain tooltip — no icon in the callout,
|
|
56437
56708
|
// no status color; an icon one presses is the callout's own status icon,
|
|
56438
56709
|
// and says "info" like the callout it opens.
|
|
@@ -56782,7 +57053,10 @@ const PickerCustom = props => {
|
|
|
56782
57053
|
dispatchCustomEvent(popupRef.current, "navi_request_open", e.detail);
|
|
56783
57054
|
},
|
|
56784
57055
|
"onnavi_request_close": e => {
|
|
56785
|
-
dispatchCustomEvent(popupRef.current, "navi_request_close", e.detail);
|
|
57056
|
+
const closing = dispatchCustomEvent(popupRef.current, "navi_request_close", e.detail);
|
|
57057
|
+
if (!closing) {
|
|
57058
|
+
e.preventDefault();
|
|
57059
|
+
}
|
|
56786
57060
|
},
|
|
56787
57061
|
children
|
|
56788
57062
|
});
|
|
@@ -56834,12 +57108,17 @@ const PickerCustom = props => {
|
|
|
56834
57108
|
event: e,
|
|
56835
57109
|
intent: "read",
|
|
56836
57110
|
allowed: () => {
|
|
56837
|
-
requestClose(e, {
|
|
57111
|
+
const closing = requestClose(e, {
|
|
56838
57112
|
isCancel: e.detail.isCancel
|
|
56839
57113
|
});
|
|
57114
|
+
if (!closing) {
|
|
57115
|
+
e.preventDefault();
|
|
57116
|
+
}
|
|
56840
57117
|
},
|
|
56841
57118
|
prevented: () => {
|
|
56842
57119
|
confirmEventRef.current = null;
|
|
57120
|
+
// Not closing either way; said back to whoever asked.
|
|
57121
|
+
e.preventDefault();
|
|
56843
57122
|
}
|
|
56844
57123
|
});
|
|
56845
57124
|
},
|
|
@@ -58978,10 +59257,11 @@ const PendingScrollRefContext = createContext(null);
|
|
|
58978
59257
|
// "muted" — keep in DOM, visible but opacified and still interactive
|
|
58979
59258
|
const SearchNoMatchModeContext = createContext("remove");
|
|
58980
59259
|
|
|
58981
|
-
//
|
|
58982
|
-
//
|
|
58983
|
-
//
|
|
58984
|
-
//
|
|
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.
|
|
58985
59265
|
const RENDER_BUDGET_DEFAULT = 100;
|
|
58986
59266
|
|
|
58987
59267
|
// Attribute used on <li> elements rendered by ListItemReal so the scroll listener
|
|
@@ -58990,8 +59270,9 @@ const REAL_LIST_ITEM_SELECTOR = `[navi-list-item-real]`;
|
|
|
58990
59270
|
// Rows standing in for content that has not arrived (see List's renderSkeleton).
|
|
58991
59271
|
const SKELETON_LIST_ITEM_CLASS = "navi_list_item_skeleton";
|
|
58992
59272
|
|
|
58993
|
-
// Carries the render window {start, end}
|
|
58994
|
-
//
|
|
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.
|
|
58995
59276
|
const RenderWindowContext = createContext(null);
|
|
58996
59277
|
// Carries List's own `columns` prop (a grid-template-columns value, e.g.
|
|
58997
59278
|
// "1fr auto auto") down to each ListItem/filler/fallback so they can render
|
|
@@ -59828,12 +60109,46 @@ const ListUI = props => {
|
|
|
59828
60109
|
searchText,
|
|
59829
60110
|
horizontal
|
|
59830
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
|
+
});
|
|
59831
60129
|
virtual.captureAnchor = captureAnchor;
|
|
59832
60130
|
virtual.virtualItemSizeSignal = virtualItemSizeSignal;
|
|
59833
60131
|
virtual.horizontal = Boolean(horizontal);
|
|
59834
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.
|
|
59835
60139
|
const getItemById = itemId => {
|
|
59836
|
-
|
|
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
|
+
};
|
|
59837
60152
|
};
|
|
59838
60153
|
const noMatchCount = tracker.noMatchCountSignal.value;
|
|
59839
60154
|
// What the list stands for, which is not always what it holds: a run saying
|
|
@@ -62373,6 +62688,9 @@ const createListVirtual = () => {
|
|
|
62373
62688
|
passId++;
|
|
62374
62689
|
nextIndex = 0;
|
|
62375
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,
|
|
62376
62694
|
setRowLocator: (ownerId, locate) => {
|
|
62377
62695
|
locatorByOwner.set(ownerId, locate);
|
|
62378
62696
|
},
|
|
@@ -62419,12 +62737,17 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62419
62737
|
* for them — which is what makes an infinitely scrolled list nothing more than
|
|
62420
62738
|
* a list that says how many rows it has.
|
|
62421
62739
|
*
|
|
62422
|
-
*
|
|
62423
|
-
*
|
|
62424
|
-
*
|
|
62425
|
-
*
|
|
62426
|
-
*
|
|
62427
|
-
*
|
|
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
|
|
62428
62751
|
* `before`/`after`/`around` (the id of a row to count from, for a source
|
|
62429
62752
|
* paginating by cursor). Answer with the rows (an array — that is all of
|
|
62430
62753
|
* them), or with a range the way a Content-Range does: `{ items, start, count }`
|
|
@@ -62438,11 +62761,11 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62438
62761
|
* copies of the JSON. The list holds the slices, the store holds the objects
|
|
62439
62762
|
* (see docs/resource.md).
|
|
62440
62763
|
*
|
|
62441
|
-
* A collection
|
|
62442
|
-
*
|
|
62443
|
-
*
|
|
62444
|
-
* with it, the way one does for anything else that is not the same
|
|
62445
|
-
* 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.
|
|
62446
62769
|
*
|
|
62447
62770
|
* A row says what it is where it is drawn: `renderItem` returns a
|
|
62448
62771
|
* `<List.Item>` carrying its own props (`selectable`, `value`, `selected`…),
|
|
@@ -62454,7 +62777,8 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62454
62777
|
*
|
|
62455
62778
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
62456
62779
|
* renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
|
|
62457
|
-
*
|
|
62780
|
+
* items?: any[],
|
|
62781
|
+
* itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
|
|
62458
62782
|
* count?: number,
|
|
62459
62783
|
* groupBy?: (item: any, index: number) => any,
|
|
62460
62784
|
* renderGroupLabel?: (item: any, index: number) => import("ignore:preact").ComponentChildren,
|
|
@@ -62469,6 +62793,13 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62469
62793
|
* What one row is, given the item and where it sits. `state.refreshing` says
|
|
62470
62794
|
* the rows drawn are the ones from before while the run reads the collection
|
|
62471
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(...)`).
|
|
62472
62803
|
* @param {(item: any, index: number) => any} [props.groupBy]
|
|
62473
62804
|
* What tells rows that belong together apart from the others — the day of a
|
|
62474
62805
|
* message, the month of a game. Consecutive rows sharing it are wrapped in a
|
|
@@ -62491,7 +62822,8 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62491
62822
|
* How many rows the run keeps in memory. Past that, the ones far from what is
|
|
62492
62823
|
* on screen are dropped (and asked for again if the user goes back) — the
|
|
62493
62824
|
* same trade the render window makes with the DOM, one order of magnitude
|
|
62494
|
-
* further out. `0` keeps
|
|
62825
|
+
* further out. `Infinity` keeps every row the run ever received; `0` keeps
|
|
62826
|
+
* only the ones around the window.
|
|
62495
62827
|
* @param {false|(index: number) => any} [props.renderSkeleton]
|
|
62496
62828
|
* What to draw for a row the run does not hold. Defaults to List's own
|
|
62497
62829
|
* `renderSkeleton`, then to a bare `<List.Item skeleton>`; `false` leaves the
|
|
@@ -62512,6 +62844,7 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
62512
62844
|
*/
|
|
62513
62845
|
const ListItems = ({
|
|
62514
62846
|
renderItem,
|
|
62847
|
+
items,
|
|
62515
62848
|
itemsAction,
|
|
62516
62849
|
count,
|
|
62517
62850
|
pageSize,
|
|
@@ -62528,6 +62861,7 @@ const ListItems = ({
|
|
|
62528
62861
|
const renderWindow = useContext(RenderWindowContext);
|
|
62529
62862
|
const separator = useContext(SeparatorContext);
|
|
62530
62863
|
const store = useItemStore({
|
|
62864
|
+
items,
|
|
62531
62865
|
count,
|
|
62532
62866
|
itemsAction,
|
|
62533
62867
|
memoryBudget,
|
|
@@ -62818,11 +63152,17 @@ const rangeIsSame = (a, b) => {
|
|
|
62818
63152
|
// are in all is enough to place it, so the pages need not be contiguous nor
|
|
62819
63153
|
// arrive in order.
|
|
62820
63154
|
const useItemStore = ({
|
|
63155
|
+
items,
|
|
62821
63156
|
count,
|
|
62822
63157
|
itemsAction,
|
|
62823
63158
|
memoryBudget,
|
|
62824
63159
|
onRequestStateChange
|
|
62825
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;
|
|
62826
63166
|
// The run's asking, on the same channel as the window it asks for — they are
|
|
62827
63167
|
// one subject: what the list is about to draw is what it goes to fetch (see
|
|
62828
63168
|
// `useRequestMissing`, and `updateRenderWindow` which logs the other half).
|
|
@@ -62844,8 +63184,28 @@ const useItemStore = ({
|
|
|
62844
63184
|
// the reader holds for it (see resource_range_reader.js).
|
|
62845
63185
|
useRef(false);
|
|
62846
63186
|
const pagesRef = useRef(null);
|
|
63187
|
+
const itemsRef = useRef(items);
|
|
63188
|
+
const itemsHeldRef = useRef(false);
|
|
62847
63189
|
let restored = false;
|
|
62848
|
-
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) {
|
|
62849
63209
|
const composition = typeof itemsAction === "function" && itemsAction.readComposition ? itemsAction.readComposition() : null;
|
|
62850
63210
|
if (composition && composition.count !== undefined) {
|
|
62851
63211
|
pagesRef.current = composition;
|
|
@@ -62892,6 +63252,16 @@ const useItemStore = ({
|
|
|
62892
63252
|
// range askable again (see the request memory just above).
|
|
62893
63253
|
const [failure, setFailure] = useState(null);
|
|
62894
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
|
+
});
|
|
62895
63265
|
// Before the first answer a run does not know how many rows it stands for.
|
|
62896
63266
|
// It stands for a windowful of them: a list that is about to be filled looks
|
|
62897
63267
|
// like rows on their way, not like an empty list.
|
|
@@ -62964,10 +63334,12 @@ const useItemStore = ({
|
|
|
62964
63334
|
// dropped and simply asked for again if the user goes back — the same
|
|
62965
63335
|
// trade the render window makes, one order of magnitude further out.
|
|
62966
63336
|
forget: (windowFrom, windowTo) => {
|
|
62967
|
-
|
|
62968
|
-
|
|
63337
|
+
if (inMemory) {
|
|
63338
|
+
// Dropping a row here would drop it for good: there is no source to
|
|
63339
|
+
// ask it back from.
|
|
62969
63340
|
return;
|
|
62970
63341
|
}
|
|
63342
|
+
const budget = memoryBudget === undefined ? ITEM_STORE_MAX_DEFAULT : memoryBudget;
|
|
62971
63343
|
const keepFrom = windowFrom - ITEM_STORE_KEEP_AROUND;
|
|
62972
63344
|
const keepTo = windowTo + ITEM_STORE_KEEP_AROUND;
|
|
62973
63345
|
if (pages.byIndex.size > budget) {
|