@jsenv/navi 0.29.156 → 0.29.158
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 +852 -342
- package/dist/jsenv_navi.js.map +98 -64
- package/dist/jsenv_navi_side_effects.js +1 -1
- package/dist/jsenv_navi_side_effects.js.map +1 -1
- package/docs/AI_INSTRUCTIONS.md +19 -6
- package/docs/actions.md +87 -3
- package/docs/control_object.md +13 -6
- package/docs/error_handling.md +39 -7
- package/docs/form_changed.md +24 -8
- package/docs/interactions.md +32 -10
- package/docs/popup_open.md +1 -1
- package/docs/resource.md +93 -13
- package/docs/route_transitions.md +6 -2
- package/docs/testid.md +22 -4
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
6
6
|
export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
|
|
7
|
-
import { elementIsFocusable, 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, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit,
|
|
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, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, scrollIntoViewScoped, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
8
8
|
export { chainEvent, clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
9
9
|
import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
|
|
10
10
|
import { isValidElement, h, Fragment, createContext, render, toChildArray, options, cloneElement } from "preact";
|
|
@@ -888,6 +888,157 @@ const isPlainObject$2 = obj => {
|
|
|
888
888
|
return Object.getPrototypeOf(obj) === proto || Object.getPrototypeOf(obj) === null;
|
|
889
889
|
};
|
|
890
890
|
|
|
891
|
+
const isSignal = (value) => {
|
|
892
|
+
return getSignalType(value) !== null;
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
const BRAND_SYMBOL = Symbol.for("preact-signals");
|
|
896
|
+
const getSignalType = (value) => {
|
|
897
|
+
if (!value || typeof value !== "object") {
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (value.brand !== BRAND_SYMBOL) {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (typeof value._fn === "function") {
|
|
906
|
+
return "computed";
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return "signal";
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
const MAX_ENTRIES = 5;
|
|
913
|
+
|
|
914
|
+
const stringifyForDisplay = (
|
|
915
|
+
value,
|
|
916
|
+
maxDepth = 2,
|
|
917
|
+
currentDepth = 0,
|
|
918
|
+
options = {},
|
|
919
|
+
) => {
|
|
920
|
+
const { asFunctionArgs = false } = options;
|
|
921
|
+
const indent = " ".repeat(currentDepth);
|
|
922
|
+
const nextIndent = " ".repeat(currentDepth + 1);
|
|
923
|
+
|
|
924
|
+
if (currentDepth >= maxDepth) {
|
|
925
|
+
return typeof value === "object" && value !== null
|
|
926
|
+
? "[Object]"
|
|
927
|
+
: String(value);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (value === null) {
|
|
931
|
+
return "null";
|
|
932
|
+
}
|
|
933
|
+
if (value === undefined) {
|
|
934
|
+
return "undefined";
|
|
935
|
+
}
|
|
936
|
+
if (typeof value === "string") {
|
|
937
|
+
return `"${value}"`;
|
|
938
|
+
}
|
|
939
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
940
|
+
return String(value);
|
|
941
|
+
}
|
|
942
|
+
if (typeof value === "function") {
|
|
943
|
+
return `[Function ${value.name || "anonymous"}]`;
|
|
944
|
+
}
|
|
945
|
+
if (value instanceof Date) {
|
|
946
|
+
return `Date(${value.toISOString()})`;
|
|
947
|
+
}
|
|
948
|
+
if (value instanceof RegExp) {
|
|
949
|
+
return value.toString();
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (Array.isArray(value)) {
|
|
953
|
+
const openBracket = asFunctionArgs ? "(" : "[";
|
|
954
|
+
const closeBracket = asFunctionArgs ? ")" : "]";
|
|
955
|
+
|
|
956
|
+
if (value.length === 0) return `${openBracket}${closeBracket}`;
|
|
957
|
+
|
|
958
|
+
// Display arrays with only one element on a single line
|
|
959
|
+
if (value.length === 1) {
|
|
960
|
+
const item = stringifyForDisplay(
|
|
961
|
+
value[0],
|
|
962
|
+
maxDepth,
|
|
963
|
+
currentDepth + 1,
|
|
964
|
+
// Remove asFunctionArgs for nested calls
|
|
965
|
+
{ ...options, asFunctionArgs: false },
|
|
966
|
+
);
|
|
967
|
+
return `${openBracket}${item}${closeBracket}`;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (value.length > MAX_ENTRIES) {
|
|
971
|
+
const preview = value
|
|
972
|
+
.slice(0, MAX_ENTRIES)
|
|
973
|
+
.map(
|
|
974
|
+
(v) =>
|
|
975
|
+
`${nextIndent}${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
|
|
976
|
+
);
|
|
977
|
+
return `${openBracket}\n${preview.join(",\n")},\n${nextIndent}...${value.length - MAX_ENTRIES} more\n${indent}${closeBracket}`;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const items = value.map(
|
|
981
|
+
(v) =>
|
|
982
|
+
`${nextIndent}${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
|
|
983
|
+
);
|
|
984
|
+
return `${openBracket}\n${items.join(",\n")}\n${indent}${closeBracket}`;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (typeof value === "object") {
|
|
988
|
+
const signalType = getSignalType(value);
|
|
989
|
+
if (signalType) {
|
|
990
|
+
const signalValue = value.peek();
|
|
991
|
+
const prefix = signalType === "computed" ? "computed" : "signal";
|
|
992
|
+
return `${prefix}(${stringifyForDisplay(signalValue, maxDepth, currentDepth, { ...options, asFunctionArgs: false })})`;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const entries = Object.entries(value);
|
|
996
|
+
if (entries.length === 0) return "{}";
|
|
997
|
+
|
|
998
|
+
// ✅ Inclure les clés avec valeurs undefined/null
|
|
999
|
+
const allEntries = [];
|
|
1000
|
+
for (const [key, val] of entries) {
|
|
1001
|
+
allEntries.push([key, val]);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Ajouter les clés avec undefined (que Object.entries omet)
|
|
1005
|
+
const descriptor = Object.getOwnPropertyDescriptors(value);
|
|
1006
|
+
for (const [key, desc] of Object.entries(descriptor)) {
|
|
1007
|
+
if (desc.value === undefined && !entries.some(([k]) => k === key)) {
|
|
1008
|
+
allEntries.push([key, undefined]);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Display objects with only one key on a single line
|
|
1013
|
+
if (allEntries.length === 1) {
|
|
1014
|
+
const [key, val] = allEntries[0];
|
|
1015
|
+
const valueStr = stringifyForDisplay(val, maxDepth, currentDepth + 1, {
|
|
1016
|
+
...options,
|
|
1017
|
+
asFunctionArgs: false,
|
|
1018
|
+
});
|
|
1019
|
+
return `{ ${key}: ${valueStr} }`;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (allEntries.length > MAX_ENTRIES) {
|
|
1023
|
+
const preview = allEntries
|
|
1024
|
+
.slice(0, MAX_ENTRIES)
|
|
1025
|
+
.map(
|
|
1026
|
+
([k, v]) =>
|
|
1027
|
+
`${nextIndent}${k}: ${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
|
|
1028
|
+
);
|
|
1029
|
+
return `{\n${preview.join(",\n")},\n${nextIndent}...${allEntries.length - MAX_ENTRIES} more\n${indent}}`;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const pairs = allEntries.map(
|
|
1033
|
+
([k, v]) =>
|
|
1034
|
+
`${nextIndent}${k}: ${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
|
|
1035
|
+
);
|
|
1036
|
+
return `{\n${pairs.join(",\n")}\n${indent}}`;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
return String(value);
|
|
1040
|
+
};
|
|
1041
|
+
|
|
891
1042
|
/*
|
|
892
1043
|
* Deep structural equality for arbitrary JS values — what `===` can't do but this
|
|
893
1044
|
* codebase constantly needs: memoization cache keys ({ id: 1 } equal to { id: 1 }),
|
|
@@ -898,9 +1049,9 @@ const isPlainObject$2 = obj => {
|
|
|
898
1049
|
* "wrong" for equality purposes: NaN equals NaN, Date compared by time value,
|
|
899
1050
|
* cycles don't loop (a set of the pairs being compared guards circular refs),
|
|
900
1051
|
* and same-type is required
|
|
901
|
-
* before descending. Functions, and objects with nothing enumerable to
|
|
902
|
-
* (a Set, a Map, an element, a URL), are equal by reference only —
|
|
903
|
-
* them says whether two are "the same". Cheap paths run first: reference
|
|
1052
|
+
* before descending. Functions, signals, and objects with nothing enumerable to
|
|
1053
|
+
* compare (a Set, a Map, an element, a URL), are equal by reference only —
|
|
1054
|
+
* nothing in them says whether two are "the same". Cheap paths run first: reference
|
|
904
1055
|
* equality, then the identity short-circuit below, then array length before
|
|
905
1056
|
* element-by-element.
|
|
906
1057
|
*
|
|
@@ -918,6 +1069,7 @@ const isPlainObject$2 = obj => {
|
|
|
918
1069
|
* compareTwoJsValues(a, b); // true immediately, no property walk
|
|
919
1070
|
*/
|
|
920
1071
|
|
|
1072
|
+
|
|
921
1073
|
// Marks objects with a conceptual identity that transcends reference equality —
|
|
922
1074
|
// see the file comment. Symbol.for keeps it one shared symbol across modules.
|
|
923
1075
|
const SYMBOL_IDENTITY = Symbol.for("navi_object_identity");
|
|
@@ -1060,6 +1212,15 @@ const compareTwoJsValues = (
|
|
|
1060
1212
|
if (aIsDate) {
|
|
1061
1213
|
return a.getTime() === b.getTime();
|
|
1062
1214
|
}
|
|
1215
|
+
// A signal is a source of truth, not a value: two of them holding the same
|
|
1216
|
+
// thing right now say nothing about the next tick, and one may change while
|
|
1217
|
+
// the other does not. Their internals (the current value, a version
|
|
1218
|
+
// counter) are enumerable, so without this they would be walked and two
|
|
1219
|
+
// unrelated signals would pass for one — a params signal owned by one
|
|
1220
|
+
// screen would then be handed the action bound to another's.
|
|
1221
|
+
if (isSignal(a) || isSignal(b)) {
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1063
1224
|
const aKeys = Object.keys(a);
|
|
1064
1225
|
const bKeys = Object.keys(b);
|
|
1065
1226
|
if (aKeys.length !== bKeys.length) {
|
|
@@ -1142,159 +1303,37 @@ const debounceSignal = (
|
|
|
1142
1303
|
clearTimeout(timeoutId);
|
|
1143
1304
|
debouncedSignal.value = latestValue;
|
|
1144
1305
|
};
|
|
1306
|
+
// Whoever reads the debounced value gets the previous one during the delay,
|
|
1307
|
+
// with nothing saying a newer one is on its way. This says it.
|
|
1308
|
+
debouncedSignal.settlingSignal = computed(() => {
|
|
1309
|
+
return !compareTwoJsValues(signalToDebounce.value, debouncedSignal.value);
|
|
1310
|
+
});
|
|
1145
1311
|
|
|
1146
1312
|
return debouncedSignal;
|
|
1147
1313
|
};
|
|
1148
1314
|
|
|
1149
|
-
const
|
|
1150
|
-
return getSignalType(value) !== null;
|
|
1151
|
-
};
|
|
1152
|
-
|
|
1153
|
-
const BRAND_SYMBOL = Symbol.for("preact-signals");
|
|
1154
|
-
const getSignalType = (value) => {
|
|
1155
|
-
if (!value || typeof value !== "object") {
|
|
1156
|
-
return null;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
if (value.brand !== BRAND_SYMBOL) {
|
|
1160
|
-
return null;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
if (typeof value._fn === "function") {
|
|
1164
|
-
return "computed";
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
return "signal";
|
|
1168
|
-
};
|
|
1169
|
-
|
|
1170
|
-
const MAX_ENTRIES = 5;
|
|
1171
|
-
|
|
1172
|
-
const stringifyForDisplay = (
|
|
1173
|
-
value,
|
|
1174
|
-
maxDepth = 2,
|
|
1175
|
-
currentDepth = 0,
|
|
1176
|
-
options = {},
|
|
1177
|
-
) => {
|
|
1178
|
-
const { asFunctionArgs = false } = options;
|
|
1179
|
-
const indent = " ".repeat(currentDepth);
|
|
1180
|
-
const nextIndent = " ".repeat(currentDepth + 1);
|
|
1181
|
-
|
|
1182
|
-
if (currentDepth >= maxDepth) {
|
|
1183
|
-
return typeof value === "object" && value !== null
|
|
1184
|
-
? "[Object]"
|
|
1185
|
-
: String(value);
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
if (value === null) {
|
|
1189
|
-
return "null";
|
|
1190
|
-
}
|
|
1191
|
-
if (value === undefined) {
|
|
1192
|
-
return "undefined";
|
|
1193
|
-
}
|
|
1194
|
-
if (typeof value === "string") {
|
|
1195
|
-
return `"${value}"`;
|
|
1196
|
-
}
|
|
1197
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
1198
|
-
return String(value);
|
|
1199
|
-
}
|
|
1200
|
-
if (typeof value === "function") {
|
|
1201
|
-
return `[Function ${value.name || "anonymous"}]`;
|
|
1202
|
-
}
|
|
1203
|
-
if (value instanceof Date) {
|
|
1204
|
-
return `Date(${value.toISOString()})`;
|
|
1205
|
-
}
|
|
1206
|
-
if (value instanceof RegExp) {
|
|
1207
|
-
return value.toString();
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
if (Array.isArray(value)) {
|
|
1211
|
-
const openBracket = asFunctionArgs ? "(" : "[";
|
|
1212
|
-
const closeBracket = asFunctionArgs ? ")" : "]";
|
|
1213
|
-
|
|
1214
|
-
if (value.length === 0) return `${openBracket}${closeBracket}`;
|
|
1315
|
+
const debouncedSignalCache = new WeakMap();
|
|
1215
1316
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
if (value.length > MAX_ENTRIES) {
|
|
1229
|
-
const preview = value
|
|
1230
|
-
.slice(0, MAX_ENTRIES)
|
|
1231
|
-
.map(
|
|
1232
|
-
(v) =>
|
|
1233
|
-
`${nextIndent}${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
|
|
1234
|
-
);
|
|
1235
|
-
return `${openBracket}\n${preview.join(",\n")},\n${nextIndent}...${value.length - MAX_ENTRIES} more\n${indent}${closeBracket}`;
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
const items = value.map(
|
|
1239
|
-
(v) =>
|
|
1240
|
-
`${nextIndent}${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
|
|
1241
|
-
);
|
|
1242
|
-
return `${openBracket}\n${items.join(",\n")}\n${indent}${closeBracket}`;
|
|
1317
|
+
/**
|
|
1318
|
+
* The one debounced signal for a given source and delay.
|
|
1319
|
+
*
|
|
1320
|
+
* Two callers asking the same question of the same signal must wait on the same
|
|
1321
|
+
* answer: a debounced signal per caller is a timer per caller, and the actions
|
|
1322
|
+
* derived from them are then several instances of what is one request.
|
|
1323
|
+
*/
|
|
1324
|
+
const getDebouncedSignal = (signalToDebounce, delay) => {
|
|
1325
|
+
let byDelay = debouncedSignalCache.get(signalToDebounce);
|
|
1326
|
+
if (!byDelay) {
|
|
1327
|
+
byDelay = new Map();
|
|
1328
|
+
debouncedSignalCache.set(signalToDebounce, byDelay);
|
|
1243
1329
|
}
|
|
1244
|
-
|
|
1245
|
-
if (
|
|
1246
|
-
|
|
1247
|
-
if (signalType) {
|
|
1248
|
-
const signalValue = value.peek();
|
|
1249
|
-
const prefix = signalType === "computed" ? "computed" : "signal";
|
|
1250
|
-
return `${prefix}(${stringifyForDisplay(signalValue, maxDepth, currentDepth, { ...options, asFunctionArgs: false })})`;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
const entries = Object.entries(value);
|
|
1254
|
-
if (entries.length === 0) return "{}";
|
|
1255
|
-
|
|
1256
|
-
// ✅ Inclure les clés avec valeurs undefined/null
|
|
1257
|
-
const allEntries = [];
|
|
1258
|
-
for (const [key, val] of entries) {
|
|
1259
|
-
allEntries.push([key, val]);
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
// Ajouter les clés avec undefined (que Object.entries omet)
|
|
1263
|
-
const descriptor = Object.getOwnPropertyDescriptors(value);
|
|
1264
|
-
for (const [key, desc] of Object.entries(descriptor)) {
|
|
1265
|
-
if (desc.value === undefined && !entries.some(([k]) => k === key)) {
|
|
1266
|
-
allEntries.push([key, undefined]);
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
// Display objects with only one key on a single line
|
|
1271
|
-
if (allEntries.length === 1) {
|
|
1272
|
-
const [key, val] = allEntries[0];
|
|
1273
|
-
const valueStr = stringifyForDisplay(val, maxDepth, currentDepth + 1, {
|
|
1274
|
-
...options,
|
|
1275
|
-
asFunctionArgs: false,
|
|
1276
|
-
});
|
|
1277
|
-
return `{ ${key}: ${valueStr} }`;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
if (allEntries.length > MAX_ENTRIES) {
|
|
1281
|
-
const preview = allEntries
|
|
1282
|
-
.slice(0, MAX_ENTRIES)
|
|
1283
|
-
.map(
|
|
1284
|
-
([k, v]) =>
|
|
1285
|
-
`${nextIndent}${k}: ${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
|
|
1286
|
-
);
|
|
1287
|
-
return `{\n${preview.join(",\n")},\n${nextIndent}...${allEntries.length - MAX_ENTRIES} more\n${indent}}`;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
const pairs = allEntries.map(
|
|
1291
|
-
([k, v]) =>
|
|
1292
|
-
`${nextIndent}${k}: ${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
|
|
1293
|
-
);
|
|
1294
|
-
return `{\n${pairs.join(",\n")}\n${indent}}`;
|
|
1330
|
+
const existing = byDelay.get(delay);
|
|
1331
|
+
if (existing) {
|
|
1332
|
+
return existing;
|
|
1295
1333
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1334
|
+
const debouncedSignal = debounceSignal(signalToDebounce, { delay });
|
|
1335
|
+
byDelay.set(delay, debouncedSignal);
|
|
1336
|
+
return debouncedSignal;
|
|
1298
1337
|
};
|
|
1299
1338
|
|
|
1300
1339
|
/**
|
|
@@ -3145,8 +3184,11 @@ const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
|
|
|
3145
3184
|
* - prerun actions may have no other reference yet, so
|
|
3146
3185
|
* prerunProtectionRegistry pins them for a few minutes.
|
|
3147
3186
|
*
|
|
3148
|
-
*
|
|
3149
|
-
*
|
|
3187
|
+
* A failing run rejects, and writes its error into errorSignal as well. The
|
|
3188
|
+
* callers that cannot take a rejection — a run started from a signal effect, a
|
|
3189
|
+
* control that already draws the failure — swallow it where they start it, and
|
|
3190
|
+
* that is where an error nobody displayed is reported once, by one rule (see
|
|
3191
|
+
* run_unwatched.js and action_error_report.js).
|
|
3150
3192
|
*
|
|
3151
3193
|
* Blast radius: this file is the substrate of routing (route_action.js,
|
|
3152
3194
|
* action_run_effect.js), resources (state/rest) and every control's `action`
|
|
@@ -3157,6 +3199,10 @@ const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
|
|
|
3157
3199
|
* routing or lists.
|
|
3158
3200
|
*/
|
|
3159
3201
|
|
|
3202
|
+
// Params waiting to settle exist only under a debounced binding; every other
|
|
3203
|
+
// action answers "no" so nobody has to check whether it can be asked.
|
|
3204
|
+
const NOT_SETTLING_SIGNAL = signal(false);
|
|
3205
|
+
|
|
3160
3206
|
let DEBUG$2 = false;
|
|
3161
3207
|
const enableDebugActions = () => {
|
|
3162
3208
|
DEBUG$2 = true;
|
|
@@ -3525,16 +3571,27 @@ ${lines.join("\n")}`);
|
|
|
3525
3571
|
|
|
3526
3572
|
const actionToRunPrivateProperties =
|
|
3527
3573
|
getActionPrivateProperties(actionToPrerunOrRun);
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3574
|
+
let performRunResult;
|
|
3575
|
+
try {
|
|
3576
|
+
performRunResult = actionToRunPrivateProperties.performRun({
|
|
3577
|
+
globalAbortSignal,
|
|
3578
|
+
abortSignal: effectiveSignal,
|
|
3579
|
+
reason,
|
|
3580
|
+
event,
|
|
3581
|
+
isPrerun,
|
|
3582
|
+
onComplete,
|
|
3583
|
+
onAbort,
|
|
3584
|
+
onError,
|
|
3585
|
+
});
|
|
3586
|
+
} catch (error) {
|
|
3587
|
+
// A synchronous callback that threw. Caught here so the update carries
|
|
3588
|
+
// on — the other actions of the same update have nothing to do with
|
|
3589
|
+
// this one — and given back further down to whoever asked for THIS
|
|
3590
|
+
// action, still as a throw.
|
|
3591
|
+
activationWeakSet.add(actionToPrerunOrRun);
|
|
3592
|
+
resultArray.push({ type: "sync_error", error });
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3538
3595
|
activationWeakSet.add(actionToPrerunOrRun);
|
|
3539
3596
|
|
|
3540
3597
|
if (performRunResult && typeof performRunResult.then === "function") {
|
|
@@ -3580,14 +3637,30 @@ ${lines.join("\n")}`);
|
|
|
3580
3637
|
requestedResult = null;
|
|
3581
3638
|
} else if (hasAsync) {
|
|
3582
3639
|
requestedResult = Promise.all(
|
|
3583
|
-
resultArray.map((item) =>
|
|
3584
|
-
item.type === "sync"
|
|
3585
|
-
|
|
3640
|
+
resultArray.map((item) => {
|
|
3641
|
+
if (item.type === "sync") {
|
|
3642
|
+
return item.result;
|
|
3643
|
+
}
|
|
3644
|
+
if (item.type === "sync_error") {
|
|
3645
|
+
return Promise.reject(item.error);
|
|
3646
|
+
}
|
|
3647
|
+
return item.promise;
|
|
3648
|
+
}),
|
|
3586
3649
|
);
|
|
3587
3650
|
} else {
|
|
3651
|
+
const itemFailed = resultArray.find((item) => item.type === "sync_error");
|
|
3652
|
+
if (itemFailed) {
|
|
3653
|
+
// Nothing of this update is left to do, so the failure can leave the way
|
|
3654
|
+
// it arrived: thrown, to the caller that asked for that action.
|
|
3655
|
+
throw itemFailed.error;
|
|
3656
|
+
}
|
|
3588
3657
|
requestedResult = resultArray.map((item) => item.result);
|
|
3589
3658
|
}
|
|
3590
3659
|
|
|
3660
|
+
// Settled rather than raced: a failure here is one outcome among several —
|
|
3661
|
+
// the other actions of the same update ran too, and "the update is over" must
|
|
3662
|
+
// not become "one of them failed". Waiting on this never rejects, which is
|
|
3663
|
+
// what lets a navigation be waited on (see via_navigation.js).
|
|
3591
3664
|
const allResult = allThenableArray.length
|
|
3592
3665
|
? Promise.allSettled(allThenableArray)
|
|
3593
3666
|
: null;
|
|
@@ -3816,12 +3889,53 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
3816
3889
|
...options,
|
|
3817
3890
|
});
|
|
3818
3891
|
};
|
|
3892
|
+
/**
|
|
3893
|
+
* The action instance for these params.
|
|
3894
|
+
*
|
|
3895
|
+
* @param {any} newParamsOrSignal - params, or a signal holding them (the
|
|
3896
|
+
* result then retargets itself as the signal changes), or an object whose
|
|
3897
|
+
* values may be signals.
|
|
3898
|
+
* @param {object} [options]
|
|
3899
|
+
* @param {number} [options.debounce] - milliseconds the params must stay
|
|
3900
|
+
* stable before the instance follows them. What it buys is a screen that
|
|
3901
|
+
* owns its params — a search box, a picker — asking one question instead
|
|
3902
|
+
* of one per keystroke, without an effect: the binding IS the delay.
|
|
3903
|
+
* Only for a signal; params that cannot change have nothing to settle.
|
|
3904
|
+
* During the delay the instance is still the previous one, holding the
|
|
3905
|
+
* previous answer — `paramsSettlingSignal` is what says a newer one is
|
|
3906
|
+
* coming, and `useAsyncData(action, { loading: true })` reads it.
|
|
3907
|
+
*/
|
|
3819
3908
|
const bindParams = (newParamsOrSignal, options = {}) => {
|
|
3909
|
+
const { debounce, ...optionsWithoutDebounce } = options;
|
|
3910
|
+
if (debounce) {
|
|
3911
|
+
if (!isSignal(newParamsOrSignal)) {
|
|
3912
|
+
console.warn(
|
|
3913
|
+
`bindParams({ debounce }) expects a signal, received ${typeof newParamsOrSignal}: params that cannot change have nothing to settle.`,
|
|
3914
|
+
);
|
|
3915
|
+
} else {
|
|
3916
|
+
// The delay lives in a signal derived from theirs, one per (signal,
|
|
3917
|
+
// delay): the cache below then keys on it like on any other signal,
|
|
3918
|
+
// so two call sites asking the same question with the same delay get
|
|
3919
|
+
// the one instance, and two different delays get two.
|
|
3920
|
+
const debouncedParamsSignal = getDebouncedSignal(
|
|
3921
|
+
newParamsOrSignal,
|
|
3922
|
+
debounce,
|
|
3923
|
+
);
|
|
3924
|
+
return bindParams(debouncedParamsSignal, {
|
|
3925
|
+
...optionsWithoutDebounce,
|
|
3926
|
+
syncParams: debouncedParamsSignal.flush,
|
|
3927
|
+
paramsSettlingSignal: debouncedParamsSignal.settlingSignal,
|
|
3928
|
+
});
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3820
3931
|
const existingChildAction = childActionWeakMap.get(newParamsOrSignal);
|
|
3821
3932
|
if (existingChildAction) {
|
|
3822
3933
|
return existingChildAction;
|
|
3823
3934
|
}
|
|
3824
|
-
const childAction = _bindParams(
|
|
3935
|
+
const childAction = _bindParams(
|
|
3936
|
+
newParamsOrSignal,
|
|
3937
|
+
optionsWithoutDebounce,
|
|
3938
|
+
);
|
|
3825
3939
|
childActionWeakMap.set(newParamsOrSignal, childAction);
|
|
3826
3940
|
if (childAction !== action) {
|
|
3827
3941
|
// binding that added nothing resolves to the action itself; it must
|
|
@@ -3964,6 +4078,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
3964
4078
|
},
|
|
3965
4079
|
|
|
3966
4080
|
paramsSignal,
|
|
4081
|
+
paramsSettlingSignal: NOT_SETTLING_SIGNAL,
|
|
3967
4082
|
runningStateSignal,
|
|
3968
4083
|
isPrerunSignal,
|
|
3969
4084
|
valueSignal,
|
|
@@ -4154,20 +4269,31 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4154
4269
|
onError?.(error, { event, action, args });
|
|
4155
4270
|
});
|
|
4156
4271
|
|
|
4157
|
-
// The error is in errorSignal; from here it is the UI's, and running
|
|
4158
|
-
// the action is not the place to decide whether the UI wants it —
|
|
4159
|
-
// that answer does not exist yet at this instant (see
|
|
4160
|
-
// action_error_report.js). So the run never throws: it settles with
|
|
4161
|
-
// the error as its value, and being displayed or not is constated
|
|
4162
|
-
// afterwards, in one place, by one rule.
|
|
4163
4272
|
if (onError) {
|
|
4164
4273
|
// Asking for the error IS taking it.
|
|
4165
4274
|
markErrorAsDisplayedBy(error, "onError");
|
|
4166
4275
|
}
|
|
4167
|
-
|
|
4276
|
+
// Handed back, not delivered: failRun decides whether this leaves as
|
|
4277
|
+
// a throw or as a rejection, because only the caller below knows
|
|
4278
|
+
// which of the two this run was. The error is in errorSignal either
|
|
4279
|
+
// way — a screen reads it there, whether or not anything was waiting.
|
|
4168
4280
|
return error;
|
|
4169
4281
|
};
|
|
4170
4282
|
|
|
4283
|
+
// A failure leaves the run the way it came: thrown when the callback
|
|
4284
|
+
// threw synchronously — code that knows this action is synchronous
|
|
4285
|
+
// writes a plain `try/catch` around the call and has to find it there —
|
|
4286
|
+
// and rejected when the run was asynchronous, since the throw below
|
|
4287
|
+
// then happens inside a `then`. An abort is neither: it settles with
|
|
4288
|
+
// its reason as its value, the way it always has.
|
|
4289
|
+
const failRun = (error) => {
|
|
4290
|
+
const errorSettled = onRunError(error);
|
|
4291
|
+
if (runningStateSignal.peek() === ABORTED) {
|
|
4292
|
+
return errorSettled;
|
|
4293
|
+
}
|
|
4294
|
+
throw errorSettled;
|
|
4295
|
+
};
|
|
4296
|
+
|
|
4171
4297
|
try {
|
|
4172
4298
|
const thenableArray = [];
|
|
4173
4299
|
const callbackResult = callback(...args);
|
|
@@ -4205,12 +4331,12 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4205
4331
|
}
|
|
4206
4332
|
return Promise.all(thenableArray).then(() => {
|
|
4207
4333
|
if (rejected) {
|
|
4208
|
-
return
|
|
4334
|
+
return failRun(rejectedValue);
|
|
4209
4335
|
}
|
|
4210
4336
|
return onRunEnd();
|
|
4211
4337
|
});
|
|
4212
4338
|
} catch (e) {
|
|
4213
|
-
return
|
|
4339
|
+
return failRun(e);
|
|
4214
4340
|
}
|
|
4215
4341
|
};
|
|
4216
4342
|
|
|
@@ -4297,6 +4423,7 @@ const createActionProxyFromSignal = (
|
|
|
4297
4423
|
inheritData = true,
|
|
4298
4424
|
onChange,
|
|
4299
4425
|
syncParams,
|
|
4426
|
+
paramsSettlingSignal = NOT_SETTLING_SIGNAL,
|
|
4300
4427
|
} = {},
|
|
4301
4428
|
) => {
|
|
4302
4429
|
const actionTargetChangeCallbackSet = new Set();
|
|
@@ -4473,6 +4600,7 @@ const createActionProxyFromSignal = (
|
|
|
4473
4600
|
meta: action.meta,
|
|
4474
4601
|
|
|
4475
4602
|
paramsSignal: proxyParamsSignal,
|
|
4603
|
+
paramsSettlingSignal,
|
|
4476
4604
|
isPrerunSignal: proxySignal("isPrerunSignal", "isPrerun"),
|
|
4477
4605
|
runningStateSignal: proxySignal("runningStateSignal", "runningState"),
|
|
4478
4606
|
errorSignal: proxySignal("errorSignal", "error"),
|
|
@@ -4618,6 +4746,46 @@ const isPlainObject = (obj) => {
|
|
|
4618
4746
|
);
|
|
4619
4747
|
};
|
|
4620
4748
|
|
|
4749
|
+
/**
|
|
4750
|
+
* Starts a run navi has nobody to await.
|
|
4751
|
+
*
|
|
4752
|
+
* A failing run does not stay quiet: it throws when the callback threw
|
|
4753
|
+
* synchronously, and rejects when it was asynchronous (see failRun in
|
|
4754
|
+
* actions.js). That is what makes `ACTION(params)` behave like any other call
|
|
4755
|
+
* that can fail — and code that lets such a failure go gets the runtime's own
|
|
4756
|
+
* unhandled-error report, which is the right answer for it.
|
|
4757
|
+
*
|
|
4758
|
+
* Some runs have no such caller by construction — one started from a signal
|
|
4759
|
+
* effect because its params changed, one whose failure the control that started
|
|
4760
|
+
* it already draws, a routing whose result every caller drops. Their failure
|
|
4761
|
+
* would be an anonymous unhandled one, naming the machinery instead of what
|
|
4762
|
+
* failed. They start here, which is why the run itself is passed rather than
|
|
4763
|
+
* its result: both deliveries have to be taken.
|
|
4764
|
+
*
|
|
4765
|
+
* And taking them is what obliges this place to ask the other question: did
|
|
4766
|
+
* anything ever display this error? The answer does not exist yet at this
|
|
4767
|
+
* instant — a route action fails before the page that shows it exists — so it is
|
|
4768
|
+
* asked with a deadline, once, in action_error_report.js.
|
|
4769
|
+
*
|
|
4770
|
+
* The error is never hidden: it stays in the action's `errorSignal`, which is
|
|
4771
|
+
* where a screen reads it.
|
|
4772
|
+
*/
|
|
4773
|
+
const runUnwatched = (startRun) => {
|
|
4774
|
+
let result;
|
|
4775
|
+
try {
|
|
4776
|
+
result = startRun();
|
|
4777
|
+
} catch (error) {
|
|
4778
|
+
reportErrorIfNobodyDisplaysIt(error);
|
|
4779
|
+
return undefined;
|
|
4780
|
+
}
|
|
4781
|
+
if (result && typeof result.catch === "function") {
|
|
4782
|
+
result.catch((error) => {
|
|
4783
|
+
reportErrorIfNobodyDisplaysIt(error);
|
|
4784
|
+
});
|
|
4785
|
+
}
|
|
4786
|
+
return result;
|
|
4787
|
+
};
|
|
4788
|
+
|
|
4621
4789
|
/**
|
|
4622
4790
|
* Reactively runs an action whenever the params derived from signals change.
|
|
4623
4791
|
*
|
|
@@ -4629,7 +4797,9 @@ const isPlainObject = (obj) => {
|
|
|
4629
4797
|
* @param {number} [options.debounce] - When set, the action is only run once the derived params
|
|
4630
4798
|
* have been stable for this many milliseconds. Useful to avoid firing a backend call on every
|
|
4631
4799
|
* keystroke: set `debounce: 500` and the request is sent only after the user stops interacting
|
|
4632
|
-
* with the filters for 500 ms.
|
|
4800
|
+
* with the filters for 500 ms. During that delay the action still holds the previous answer;
|
|
4801
|
+
* `action.paramsSettlingSignal` is what says a newer one is coming, and
|
|
4802
|
+
* `useAsyncData(action, { loading: true })` reads it.
|
|
4633
4803
|
*
|
|
4634
4804
|
* Example — auto-refresh a result list while the user tweaks filters:
|
|
4635
4805
|
* ```js
|
|
@@ -4645,12 +4815,6 @@ const isPlainObject = (obj) => {
|
|
|
4645
4815
|
// one — in dev, an error overlay thrown over a page that is already saying what
|
|
4646
4816
|
// went wrong. Nothing is lost by dropping it: the failure is held by the action
|
|
4647
4817
|
// itself, and whoever reads it (useAsyncData, <Button action>) is what shows it.
|
|
4648
|
-
const runUnwatched = (result) => {
|
|
4649
|
-
if (result && typeof result.catch === "function") {
|
|
4650
|
-
result.catch(() => {});
|
|
4651
|
-
}
|
|
4652
|
-
};
|
|
4653
|
-
|
|
4654
4818
|
const actionRunEffect = (
|
|
4655
4819
|
action,
|
|
4656
4820
|
deriveActionParamsFromSignals,
|
|
@@ -4659,7 +4823,7 @@ const actionRunEffect = (
|
|
|
4659
4823
|
if (typeof action === "function") {
|
|
4660
4824
|
action = createAction(action);
|
|
4661
4825
|
}
|
|
4662
|
-
|
|
4826
|
+
const actionParamsSignal = computed(() => {
|
|
4663
4827
|
const params = deriveActionParamsFromSignals();
|
|
4664
4828
|
action.debug(
|
|
4665
4829
|
`Derived params for action "${action}": ${stringifyForDisplay(params)}`,
|
|
@@ -4677,14 +4841,8 @@ const actionRunEffect = (
|
|
|
4677
4841
|
}
|
|
4678
4842
|
return params;
|
|
4679
4843
|
});
|
|
4680
|
-
if (debounce) {
|
|
4681
|
-
actionParamsSignal = debounceSignal(actionParamsSignal, {
|
|
4682
|
-
delay: debounce,
|
|
4683
|
-
});
|
|
4684
|
-
}
|
|
4685
|
-
|
|
4686
4844
|
const actionRunnedByThisEffect = action.bindParams(actionParamsSignal, {
|
|
4687
|
-
|
|
4845
|
+
debounce,
|
|
4688
4846
|
onChange: (actionTarget, actionTargetPrevious, { explicitRunIntent }) => {
|
|
4689
4847
|
if (explicitRunIntent) {
|
|
4690
4848
|
// The caller already issued an explicit run/rerun/prerun/reset/abort —
|
|
@@ -4700,7 +4858,9 @@ const actionRunEffect = (
|
|
|
4700
4858
|
// falsy params, don't run
|
|
4701
4859
|
return;
|
|
4702
4860
|
}
|
|
4703
|
-
runUnwatched(
|
|
4861
|
+
runUnwatched(() =>
|
|
4862
|
+
actionTarget.run({ reason: "truthy params first run" }),
|
|
4863
|
+
);
|
|
4704
4864
|
return;
|
|
4705
4865
|
}
|
|
4706
4866
|
|
|
@@ -4717,18 +4877,18 @@ const actionRunEffect = (
|
|
|
4717
4877
|
}
|
|
4718
4878
|
if (!actionTargetPrevious.params) {
|
|
4719
4879
|
// coming from falsy-params state: action may already be cached, avoid unnecessary rerun
|
|
4720
|
-
runUnwatched(
|
|
4880
|
+
runUnwatched(() =>
|
|
4721
4881
|
actionTarget.run({ reason: "params restored from falsy state" }),
|
|
4722
4882
|
);
|
|
4723
4883
|
} else {
|
|
4724
|
-
runUnwatched(actionTarget.rerun({ reason: "params modified" }));
|
|
4884
|
+
runUnwatched(() => actionTarget.rerun({ reason: "params modified" }));
|
|
4725
4885
|
}
|
|
4726
4886
|
}
|
|
4727
4887
|
},
|
|
4728
4888
|
...options,
|
|
4729
4889
|
});
|
|
4730
4890
|
if (actionParamsSignal.peek()) {
|
|
4731
|
-
runUnwatched(
|
|
4891
|
+
runUnwatched(() =>
|
|
4732
4892
|
actionRunnedByThisEffect.run({ reason: "initial truthy params" }),
|
|
4733
4893
|
);
|
|
4734
4894
|
}
|
|
@@ -5610,6 +5770,16 @@ const ActionRequesterContext = createContext();
|
|
|
5610
5770
|
* Whichever mode, the affordance's own handler runs from inside its own gate
|
|
5611
5771
|
* rather than from the DOM: a caller's `onClick` fires before any of this, and
|
|
5612
5772
|
* would go off from a button drawn greyed.
|
|
5773
|
+
*
|
|
5774
|
+
* These two answer about the GESTURE. Whose VALUE an element carries is a third
|
|
5775
|
+
* question, and `standalone` is where it is answered (see useUIStateController).
|
|
5776
|
+
* They are said separately because they genuinely come apart: a door that holds
|
|
5777
|
+
* no value of its own can still write into the control around it — it is
|
|
5778
|
+
* `standalone` and must still shut when that control is read-only — and an
|
|
5779
|
+
* affordance can claim a press without ever being a field. Something that is
|
|
5780
|
+
* its own on all three counts says all three (a diskette filing a name into the
|
|
5781
|
+
* reader's own address book: `selfInteractions="click"`,
|
|
5782
|
+
* `whenSelfInteractionsBlocked="ignore"`, `standalone`).
|
|
5613
5783
|
*/
|
|
5614
5784
|
|
|
5615
5785
|
|
|
@@ -10162,6 +10332,35 @@ const watchActionCompletion = (element, dispatchAction) => {
|
|
|
10162
10332
|
};
|
|
10163
10333
|
};
|
|
10164
10334
|
|
|
10335
|
+
/**
|
|
10336
|
+
* What follows the control's OWN action: it runs once that action has
|
|
10337
|
+
* succeeded, and not at all otherwise.
|
|
10338
|
+
*
|
|
10339
|
+
* The outcome arrives in one of three shapes and each decides differently:
|
|
10340
|
+
* - the gate turned the request down (`result === false`): nothing happened,
|
|
10341
|
+
* so nothing follows;
|
|
10342
|
+
* - the action is running: what follows waits for it, and an error or an abort
|
|
10343
|
+
* drops it — whatever the action left in front of the user stays;
|
|
10344
|
+
* - it is already settled, or never started at all. A control with nothing to
|
|
10345
|
+
* run leaves no outcome behind, and no outcome means nothing went wrong.
|
|
10346
|
+
*/
|
|
10347
|
+
const runWhenActionSucceeded = (completion, callback) => {
|
|
10348
|
+
if (completion.result === false) {
|
|
10349
|
+
return;
|
|
10350
|
+
}
|
|
10351
|
+
if (completion.isRunning) {
|
|
10352
|
+
completion.whenSucceeded(callback);
|
|
10353
|
+
return;
|
|
10354
|
+
}
|
|
10355
|
+
let succeeded = true;
|
|
10356
|
+
completion.whenSettled(({ error, aborted }) => {
|
|
10357
|
+
succeeded = !error && !aborted;
|
|
10358
|
+
});
|
|
10359
|
+
if (succeeded) {
|
|
10360
|
+
callback();
|
|
10361
|
+
}
|
|
10362
|
+
};
|
|
10363
|
+
|
|
10165
10364
|
const tryActionAfterInteractionAllowed = (
|
|
10166
10365
|
element,
|
|
10167
10366
|
{
|
|
@@ -12822,7 +13021,10 @@ const useExecuteAction = (
|
|
|
12822
13021
|
});
|
|
12823
13022
|
};
|
|
12824
13023
|
|
|
12825
|
-
|
|
13024
|
+
// The control is already holding the failure — the error side effect
|
|
13025
|
+
// above drew the callout, or threw it at the boundary — so nothing here
|
|
13026
|
+
// is waiting on the rejection.
|
|
13027
|
+
return runUnwatched(runAction);
|
|
12826
13028
|
},
|
|
12827
13029
|
[elementRef, errorEffect],
|
|
12828
13030
|
);
|
|
@@ -15174,8 +15376,12 @@ const createResource = (
|
|
|
15174
15376
|
idKey,
|
|
15175
15377
|
uniqueKeys,
|
|
15176
15378
|
|
|
15379
|
+
// Reactive reads, not hooks: they subscribe the render that calls them to
|
|
15380
|
+
// the store's array signal. Nothing hook-shaped inside, so a loop or a
|
|
15381
|
+
// condition is fine. `useAllByIds` skips the ids the store does not hold.
|
|
15177
15382
|
useArray: () => store.arraySignal.value,
|
|
15178
15383
|
useById: (id) => store.select(idKey, id),
|
|
15384
|
+
useAllByIds: (idArray) => store.selectAll(idArray),
|
|
15179
15385
|
|
|
15180
15386
|
withParams: undefined,
|
|
15181
15387
|
one: undefined,
|
|
@@ -17332,15 +17538,11 @@ const useActionAsyncData = (action, {
|
|
|
17332
17538
|
// the wait is the ordinary one: suspended into <Loading>, or drawn by the
|
|
17333
17539
|
// component under `loading: true`, exactly as for data someone else ran.
|
|
17334
17540
|
if (run && action.runningStateSignal.peek() === IDLE && action.paramsSignal.peek() !== undefined) {
|
|
17335
|
-
|
|
17541
|
+
// Nothing waits on this run: the failure is held by the action, and this
|
|
17542
|
+
// hook is what reads it back.
|
|
17543
|
+
runUnwatched(() => action.run({
|
|
17336
17544
|
reason: "useAsyncData({ run: true })"
|
|
17337
|
-
});
|
|
17338
|
-
// Nobody awaits this run, and a rejection nobody awaits is an unhandled
|
|
17339
|
-
// one — in dev, an overlay over a component already saying what failed.
|
|
17340
|
-
// The failure is held by the action, and this hook is what reads it.
|
|
17341
|
-
if (runResult && typeof runResult.catch === "function") {
|
|
17342
|
-
runResult.catch(() => {});
|
|
17343
|
-
}
|
|
17545
|
+
}));
|
|
17344
17546
|
}
|
|
17345
17547
|
|
|
17346
17548
|
// Use peek() instead of .value to avoid subscribing this component to the signal.
|
|
@@ -17374,14 +17576,34 @@ const useActionAsyncData = (action, {
|
|
|
17374
17576
|
}
|
|
17375
17577
|
setTick(n => n + 1);
|
|
17376
17578
|
});
|
|
17579
|
+
// A debounced binding waits before it retargets, so nothing above changes
|
|
17580
|
+
// while the delay runs — but what is on screen is already out of date.
|
|
17581
|
+
let settlingNotificationIsInitial = true;
|
|
17582
|
+
const unsubscribeFromParamsSettling = action.paramsSettlingSignal.subscribe(() => {
|
|
17583
|
+
if (settlingNotificationIsInitial) {
|
|
17584
|
+
settlingNotificationIsInitial = false;
|
|
17585
|
+
return;
|
|
17586
|
+
}
|
|
17587
|
+
setTick(n => n + 1);
|
|
17588
|
+
});
|
|
17377
17589
|
return () => {
|
|
17378
17590
|
unsubscribeFromRunningState();
|
|
17379
17591
|
unsubscribeFromData();
|
|
17592
|
+
unsubscribeFromParamsSettling();
|
|
17380
17593
|
};
|
|
17381
17594
|
// Bound to the action, not to the mount: params given as a plain object
|
|
17382
17595
|
// make another action instance, and the component would otherwise stay
|
|
17383
17596
|
// subscribed to the state of the one it no longer reads.
|
|
17384
17597
|
}, [action]);
|
|
17598
|
+
|
|
17599
|
+
// The params moved and the run has not started: under a debounce the action
|
|
17600
|
+
// is still the previous one, COMPLETED with the previous answer. There is no
|
|
17601
|
+
// run to await yet, so nothing can be suspended on — but a component drawing
|
|
17602
|
+
// its own wait can say the answer on screen is on its way out, which is the
|
|
17603
|
+
// whole reason it asked for `loading`.
|
|
17604
|
+
if (loadingEffect === "use" && runningState !== FAILED && action.paramsSettlingSignal.peek()) {
|
|
17605
|
+
return [action.dataSignal.peek(), true, undefined];
|
|
17606
|
+
}
|
|
17385
17607
|
if (runningState === COMPLETED) {
|
|
17386
17608
|
return [action.dataSignal.peek(), false, undefined];
|
|
17387
17609
|
}
|
|
@@ -17431,12 +17653,6 @@ const useActionAsyncData = (action, {
|
|
|
17431
17653
|
throw actionError;
|
|
17432
17654
|
}
|
|
17433
17655
|
|
|
17434
|
-
// RUNNING with loadingEffect: "use" — return stale data + loading flag, no suspend
|
|
17435
|
-
if (loadingEffect === "use" && runningState === RUNNING) {
|
|
17436
|
-
const staleData = action.dataSignal.peek();
|
|
17437
|
-
return [staleData, true, undefined];
|
|
17438
|
-
}
|
|
17439
|
-
|
|
17440
17656
|
// An action without params has nothing to run and no one to start it: this is
|
|
17441
17657
|
// where a route action lands when its params getter returns false. It is not a
|
|
17442
17658
|
// load in progress, so there is nothing to wait for — suspending here would
|
|
@@ -17446,6 +17662,17 @@ const useActionAsyncData = (action, {
|
|
|
17446
17662
|
return [action.dataSignal.peek(), false, undefined];
|
|
17447
17663
|
}
|
|
17448
17664
|
|
|
17665
|
+
// `loading: true` says the component draws its own wait, so this hook never
|
|
17666
|
+
// suspends: running, or holding params nobody has started yet — a debounced
|
|
17667
|
+
// binding that just retargeted, an aborted run, a run this very render is
|
|
17668
|
+
// about to issue. Either way what is on screen is not the current answer,
|
|
17669
|
+
// which is what `loading` says. Suspending would not draw a spinner: it swaps
|
|
17670
|
+
// the subtree for the boundary, and everything under it remounts — a dialog
|
|
17671
|
+
// open inside it is then closed, by a hook the caller told to stay out of it.
|
|
17672
|
+
if (loadingEffect === "use") {
|
|
17673
|
+
return [action.dataSignal.peek(), true, undefined];
|
|
17674
|
+
}
|
|
17675
|
+
|
|
17449
17676
|
// IDLE or RUNNING with loadingEffect: "delegate" — suspend
|
|
17450
17677
|
const reason = runningState === RUNNING ? "loading" : "idle";
|
|
17451
17678
|
loadingRef.current = {
|
|
@@ -26222,18 +26449,12 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
26222
26449
|
// something at the first announcement has a definite place to give it back.
|
|
26223
26450
|
publishBeforeRouting({ url, ...options });
|
|
26224
26451
|
try {
|
|
26225
|
-
|
|
26226
|
-
|
|
26227
|
-
|
|
26228
|
-
|
|
26229
|
-
|
|
26230
|
-
|
|
26231
|
-
// an error nobody displays (see action_error_report.js).
|
|
26232
|
-
routingResult.catch((e) => {
|
|
26233
|
-
reportErrorIfNobodyDisplaysIt(e);
|
|
26234
|
-
});
|
|
26235
|
-
}
|
|
26236
|
-
return routingResult;
|
|
26452
|
+
// Every caller below drops this value — a click handler has nothing to do
|
|
26453
|
+
// with what the routing returns — so a rejection here would become an
|
|
26454
|
+
// anonymous unhandled one, pointing at the navigation rather than at what
|
|
26455
|
+
// failed. Taken, and passed to the one place that knows what to do with
|
|
26456
|
+
// an error nobody displays (see run_unwatched.js).
|
|
26457
|
+
return runUnwatched(() => applyRoutingTask(url, options));
|
|
26237
26458
|
} finally {
|
|
26238
26459
|
publishAfterRouting({ url, ...options });
|
|
26239
26460
|
}
|
|
@@ -28105,8 +28326,10 @@ const RouteTransitionArea = ({
|
|
|
28105
28326
|
* @param {object} to - same forms. Going from `from` to `to` plays forward,
|
|
28106
28327
|
* the reverse plays back — unless the reverse is written as a relation of
|
|
28107
28328
|
* its own, which then owns that way (a movement of its own, or `"none"` for
|
|
28108
|
-
* a plain cut).
|
|
28109
|
-
*
|
|
28329
|
+
* a plain cut). Written with the SAME movement it owns that way all the
|
|
28330
|
+
* same, so both crossings play forward and the pair can never say "back":
|
|
28331
|
+
* that pair is warned about. A change between two pages no relation was
|
|
28332
|
+
* written for plays nothing.
|
|
28110
28333
|
* @param {string|{type?: string, duration?: number|string}} [transition] -
|
|
28111
28334
|
* what plays: a type name, or `{ type, duration }` to also say how long
|
|
28112
28335
|
* (`--navi-route-transition-duration` says it for everyone otherwise).
|
|
@@ -28155,6 +28378,7 @@ const defineRouteTransition = (from, to, transition) => {
|
|
|
28155
28378
|
type,
|
|
28156
28379
|
duration
|
|
28157
28380
|
};
|
|
28381
|
+
warnAboutBothWaysWritten(relation);
|
|
28158
28382
|
relations.push(relation);
|
|
28159
28383
|
rebuildWatcher();
|
|
28160
28384
|
return () => {
|
|
@@ -28749,6 +28973,38 @@ const currentPageIndex$1 = pages => {
|
|
|
28749
28973
|
}
|
|
28750
28974
|
return currentIndex;
|
|
28751
28975
|
};
|
|
28976
|
+
|
|
28977
|
+
// Both ways of a pair written with the SAME movement. The exact way travelled
|
|
28978
|
+
// is searched before any reverse (see findRelation), so each way then finds
|
|
28979
|
+
// its own relation and BOTH play forward: the pair loses the one thing a
|
|
28980
|
+
// direction is for. That is not a resolution to fix — it is what makes
|
|
28981
|
+
// reciprocity a default — it is a definition with no reading under which it is
|
|
28982
|
+
// what the author meant, so it is said here, where both relations are known.
|
|
28983
|
+
// Narrow on purpose: a different type is the asymmetry the reverse exists for,
|
|
28984
|
+
// "none" is silence said out loud, and a different duration is a way out taken
|
|
28985
|
+
// slower.
|
|
28986
|
+
const warnAboutBothWaysWritten = ({
|
|
28987
|
+
from,
|
|
28988
|
+
to,
|
|
28989
|
+
type,
|
|
28990
|
+
duration
|
|
28991
|
+
}) => {
|
|
28992
|
+
const reverse = relations.find(candidate => samePage$1(candidate.from, to) && samePage$1(candidate.to, from));
|
|
28993
|
+
if (!reverse) {
|
|
28994
|
+
return;
|
|
28995
|
+
}
|
|
28996
|
+
if (reverse.type !== type || reverse.duration !== duration) {
|
|
28997
|
+
return;
|
|
28998
|
+
}
|
|
28999
|
+
if (type === "none") {
|
|
29000
|
+
return;
|
|
29001
|
+
}
|
|
29002
|
+
// The one already written is named first: that is the order the application
|
|
29003
|
+
// wrote them in, and the order it will find them in to fix them.
|
|
29004
|
+
const written = `${describePage(reverse.from)} → ${describePage(reverse.to)}`;
|
|
29005
|
+
const added = `${describePage(from)} → ${describePage(to)}`;
|
|
29006
|
+
warnOnce(`both-ways-written:${written}|${added}`, `${written} and ${added} are both written with the same movement, so BOTH crossings play forward and this pair can never say "back" — the back button included. A relation written for the exact way travelled wins over being the reverse of another (see findRelation), which is what makes reciprocity the default: write the way back only to give it a DIFFERENT movement, or "none" to silence it. A single crossing that walks the map backwards says so on itself instead: <Link routeTransition={{ direction: "forward" }}>, or navTo(url, { routeTransition: { direction: "forward" } }).`);
|
|
29007
|
+
};
|
|
28752
29008
|
const warnPagesBothCurrent = (pageKept, pageIgnored) => {
|
|
28753
29009
|
const kept = describePage(pageKept);
|
|
28754
29010
|
const ignored = describePage(pageIgnored);
|
|
@@ -32613,20 +32869,21 @@ const ParentUIStateControllerContext = createContext();
|
|
|
32613
32869
|
*/
|
|
32614
32870
|
const useUIStateController = (
|
|
32615
32871
|
props,
|
|
32616
|
-
{
|
|
32617
|
-
controlInfo,
|
|
32618
|
-
syncDomState,
|
|
32619
|
-
uiActionInternal,
|
|
32620
|
-
persists,
|
|
32621
|
-
allowNameless = false,
|
|
32622
|
-
} = {},
|
|
32872
|
+
{ controlInfo, syncDomState, uiActionInternal, persists, standalone } = {},
|
|
32623
32873
|
) => {
|
|
32624
32874
|
const debugPopup = useDebugPopup();
|
|
32625
32875
|
const debugInteraction = useDebugInteraction();
|
|
32626
32876
|
const debugUIState = useDebugUIState();
|
|
32627
32877
|
const debugFocus = useDebugFocus();
|
|
32628
32878
|
|
|
32629
|
-
|
|
32879
|
+
// The whole of `standalone`: with no parent to register into, the effect
|
|
32880
|
+
// below has nobody to call, and every question a group asks about its
|
|
32881
|
+
// children is asked about someone else. Read here rather than filtered by
|
|
32882
|
+
// each group shape in turn, so a control that answers for itself is out of
|
|
32883
|
+
// the value on the way up AND out of the reset, the validation cascade and
|
|
32884
|
+
// the distribution on the way down.
|
|
32885
|
+
const parentFromContext = useContext(ParentUIStateControllerContext);
|
|
32886
|
+
const parentUIStateController = standalone ? undefined : parentFromContext;
|
|
32630
32887
|
const formContext = useContext(FormContext);
|
|
32631
32888
|
if (persists === undefined && formContext) {
|
|
32632
32889
|
persists = true;
|
|
@@ -32700,7 +32957,6 @@ const useUIStateController = (
|
|
|
32700
32957
|
parentUIStateController,
|
|
32701
32958
|
parentUiStateSignalHolder,
|
|
32702
32959
|
isProxy,
|
|
32703
|
-
allowNameless,
|
|
32704
32960
|
emptyUIState,
|
|
32705
32961
|
// Set here too, not only in `update` below: a control rendered once and
|
|
32706
32962
|
// never re-rendered would otherwise never say whether it was GIVEN a
|
|
@@ -33366,18 +33622,14 @@ const GROUP_DEFAULTS = {
|
|
|
33366
33622
|
single: {
|
|
33367
33623
|
// The same exclusions canRegisterAsFacadeChild already makes below (the
|
|
33368
33624
|
// picker façade asked the very same question: which child IS the value).
|
|
33369
|
-
// Buttons and links never hold one
|
|
33370
|
-
//
|
|
33371
|
-
// box driving some other list — not the list itself, which stays a
|
|
33625
|
+
// Buttons and links never hold one. A control *carrying* navi-list is the
|
|
33626
|
+
// search box driving some other list — not the list itself, which stays a
|
|
33372
33627
|
// perfectly good single value here (one item, or the array a multiple list
|
|
33373
33628
|
// exposes). Excluding the searcher is what leaves the list alone.
|
|
33374
33629
|
childControlFilter: (child) => {
|
|
33375
33630
|
if (child.controlType === "button" || child.controlType === "link") {
|
|
33376
33631
|
return false;
|
|
33377
33632
|
}
|
|
33378
|
-
if (child.allowNameless) {
|
|
33379
|
-
return false;
|
|
33380
|
-
}
|
|
33381
33633
|
if (child.props?.["navi-list"]) {
|
|
33382
33634
|
return false;
|
|
33383
33635
|
}
|
|
@@ -33402,7 +33654,7 @@ const GROUP_DEFAULTS = {
|
|
|
33402
33654
|
aggregateChildStates: (children) => {
|
|
33403
33655
|
const groupValues = {};
|
|
33404
33656
|
for (const child of children) {
|
|
33405
|
-
const { name,
|
|
33657
|
+
const { name, emptyUIState } = child;
|
|
33406
33658
|
// A control holding nothing writes its own empty, not a hole: the key
|
|
33407
33659
|
// is in the object either way, and what is read from it keeps the shape
|
|
33408
33660
|
// the reader was promised (see resolveEmptyUIState).
|
|
@@ -33411,13 +33663,6 @@ const GROUP_DEFAULTS = {
|
|
|
33411
33663
|
? emptyUIState
|
|
33412
33664
|
: child.uiState;
|
|
33413
33665
|
if (!name) {
|
|
33414
|
-
if (allowNameless) {
|
|
33415
|
-
// A control that says it is not a field is not one, whatever it
|
|
33416
|
-
// holds: a picker used as a door holds the shape its popup draws,
|
|
33417
|
-
// and merging that in would put the popup's keys in the object as
|
|
33418
|
-
// if the door had been a group.
|
|
33419
|
-
continue;
|
|
33420
|
-
}
|
|
33421
33666
|
// A nameless GROUP is a grouping, not a value: it exists to hold its
|
|
33422
33667
|
// children together (a WheelGroup sharing navigation, a fieldset-ish
|
|
33423
33668
|
// cluster) without claiming a key of its own, so what it holds is
|
|
@@ -33530,6 +33775,7 @@ const useUIGroupStateController = (
|
|
|
33530
33775
|
uiActionInternal,
|
|
33531
33776
|
allowCapture = false,
|
|
33532
33777
|
cascadeValidationToChildren = false,
|
|
33778
|
+
standalone,
|
|
33533
33779
|
},
|
|
33534
33780
|
) => {
|
|
33535
33781
|
const debugPopup = useDebugPopup();
|
|
@@ -33565,7 +33811,11 @@ const useUIGroupStateController = (
|
|
|
33565
33811
|
`Either use a known controlType/stateType or provide aggregateChildStates and distributeChildUIState explicitly.`,
|
|
33566
33812
|
);
|
|
33567
33813
|
}
|
|
33568
|
-
|
|
33814
|
+
// See the leaf controller's own `standalone` above: the same opt-out, and a
|
|
33815
|
+
// group taking it also takes its whole subtree out — the controls inside it
|
|
33816
|
+
// register into IT, and it registers into nobody.
|
|
33817
|
+
const parentFromContext = useContext(ParentUIStateControllerContext);
|
|
33818
|
+
const parentUIStateController = standalone ? undefined : parentFromContext;
|
|
33569
33819
|
// A bound signal seeds the group the way `defaultValue` does — uncontrolled,
|
|
33570
33820
|
// with the signal's current value as what it starts on. Write-back is handled
|
|
33571
33821
|
// by applyState's own boundSignal; the read half is below: children are
|
|
@@ -34336,7 +34586,6 @@ const EMPTY_OBJECT = {};
|
|
|
34336
34586
|
*/
|
|
34337
34587
|
const useUIFacadeStateController = (props, realUIStateController) => {
|
|
34338
34588
|
const firstChildControllerRef = useRef(null);
|
|
34339
|
-
const namelessChildSetRef = useRef(new Set());
|
|
34340
34589
|
const updatingRef = useRef(false);
|
|
34341
34590
|
const debugPopup = useDebugPopup();
|
|
34342
34591
|
const debugInteraction = useDebugInteraction();
|
|
@@ -34364,16 +34613,9 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
34364
34613
|
if (childController.isProxy) {
|
|
34365
34614
|
return false;
|
|
34366
34615
|
}
|
|
34367
|
-
if (childController.allowNameless) {
|
|
34368
|
-
// A control saying it is not a field is not the one the picker talks
|
|
34369
|
-
// to: the search box above the list, the "select all" switch beside
|
|
34370
|
-
// it. It is there to help find the answer, not to be it.
|
|
34371
|
-
namelessChildSetRef.current.add(childController);
|
|
34372
|
-
return false;
|
|
34373
|
-
}
|
|
34374
34616
|
if (childController.props["navi-list"]) {
|
|
34375
|
-
// Controls with navi-list act as
|
|
34376
|
-
// not
|
|
34617
|
+
// Controls with navi-list act as list navigators driving some other
|
|
34618
|
+
// list, not as the picker's synced child.
|
|
34377
34619
|
return false;
|
|
34378
34620
|
}
|
|
34379
34621
|
return true;
|
|
@@ -34391,10 +34633,6 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
34391
34633
|
}
|
|
34392
34634
|
const child = firstChildControllerRef.current;
|
|
34393
34635
|
if (!child) {
|
|
34394
|
-
warnPopupHasNothingButNamelessControls(
|
|
34395
|
-
props,
|
|
34396
|
-
namelessChildSetRef.current,
|
|
34397
|
-
);
|
|
34398
34636
|
return;
|
|
34399
34637
|
}
|
|
34400
34638
|
updatingRef.current = true;
|
|
@@ -34429,7 +34667,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
34429
34667
|
`A picker talks to ONE control: the first one receives the picker's whole value and is the only one read back, ` +
|
|
34430
34668
|
`so this one is neither filled nor collected. ` +
|
|
34431
34669
|
`A popup holding several values needs one group around them — wrap them in a <ControlGroup>, name each control inside it, and give the picker type="object". ` +
|
|
34432
|
-
`A control that
|
|
34670
|
+
`A control that answers for itself rather than for the picker — a search box, a list acting on every touch — says so with standalone and steps out of the way.`,
|
|
34433
34671
|
child,
|
|
34434
34672
|
);
|
|
34435
34673
|
} else {
|
|
@@ -34556,11 +34794,6 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
34556
34794
|
|
|
34557
34795
|
const describePicker = (props) =>
|
|
34558
34796
|
`<Picker${props.name ? ` name="${props.name}"` : ""}${props.type ? ` type="${props.type}"` : ""}>`;
|
|
34559
|
-
const warnPopupHasNothingButNamelessControls = (props, namelessChildSet) => {
|
|
34560
|
-
{
|
|
34561
|
-
return;
|
|
34562
|
-
}
|
|
34563
|
-
};
|
|
34564
34797
|
|
|
34565
34798
|
/**
|
|
34566
34799
|
* Returns true when `e` should trigger parent notification (child → parent bubbling).
|
|
@@ -34833,19 +35066,19 @@ const ControlgroupChildrenWrapper = ({
|
|
|
34833
35066
|
*/
|
|
34834
35067
|
const useControlProps = (props, {
|
|
34835
35068
|
controlType,
|
|
34836
|
-
allowNameless: allowNamelessByDefault,
|
|
34837
35069
|
persists,
|
|
34838
35070
|
uiActionInternal
|
|
34839
35071
|
}) => {
|
|
34840
35072
|
const debugUIState = useDebugUIState();
|
|
34841
35073
|
const debugAction = useDebugAction();
|
|
34842
35074
|
|
|
34843
|
-
//
|
|
34844
|
-
//
|
|
34845
|
-
//
|
|
34846
|
-
|
|
34847
|
-
|
|
34848
|
-
|
|
35075
|
+
// This control answers for itself: whatever group it sits in expects nothing
|
|
35076
|
+
// from it and gives it nothing back. See useUIStateController, which is where
|
|
35077
|
+
// the whole of it happens.
|
|
35078
|
+
const {
|
|
35079
|
+
standalone
|
|
35080
|
+
} = props;
|
|
35081
|
+
delete props.standalone;
|
|
34849
35082
|
const idDefault = useId();
|
|
34850
35083
|
const controlId = useContext(ControlIdContext);
|
|
34851
35084
|
props.id = props.id || controlId || idDefault;
|
|
@@ -34901,7 +35134,7 @@ const useControlProps = (props, {
|
|
|
34901
35134
|
const uiStateController = useUIStateController(props, {
|
|
34902
35135
|
controlInfo,
|
|
34903
35136
|
syncDomState,
|
|
34904
|
-
|
|
35137
|
+
standalone,
|
|
34905
35138
|
persists,
|
|
34906
35139
|
uiActionInternal
|
|
34907
35140
|
});
|
|
@@ -35100,28 +35333,7 @@ const useControlProps = (props, {
|
|
|
35100
35333
|
if (!deferredCommand) {
|
|
35101
35334
|
return;
|
|
35102
35335
|
}
|
|
35103
|
-
|
|
35104
|
-
// The action was turned down (a failing constraint, a gate saying
|
|
35105
|
-
// no) — nothing happened, so nothing follows.
|
|
35106
|
-
return;
|
|
35107
|
-
}
|
|
35108
|
-
if (completion.isRunning) {
|
|
35109
|
-
completion.whenSucceeded(deferredCommand);
|
|
35110
|
-
return;
|
|
35111
|
-
}
|
|
35112
|
-
// Synchronous: already settled, and how it ended still decides. An
|
|
35113
|
-
// action that never started (nothing to run) leaves no outcome, and
|
|
35114
|
-
// the command runs as it always did.
|
|
35115
|
-
let succeeded = true;
|
|
35116
|
-
completion.whenSettled(({
|
|
35117
|
-
error,
|
|
35118
|
-
aborted
|
|
35119
|
-
}) => {
|
|
35120
|
-
succeeded = !error && !aborted;
|
|
35121
|
-
});
|
|
35122
|
-
if (succeeded) {
|
|
35123
|
-
deferredCommand();
|
|
35124
|
-
}
|
|
35336
|
+
runWhenActionSucceeded(completion, deferredCommand);
|
|
35125
35337
|
};
|
|
35126
35338
|
return {
|
|
35127
35339
|
keyDown: keyDownDefault,
|
|
@@ -35882,8 +36094,10 @@ const useControlgroupProps = (props, {
|
|
|
35882
36094
|
cascadeValidationToChildren = false
|
|
35883
36095
|
}) => {
|
|
35884
36096
|
const {
|
|
35885
|
-
action
|
|
36097
|
+
action,
|
|
36098
|
+
standalone
|
|
35886
36099
|
} = props;
|
|
36100
|
+
delete props.standalone;
|
|
35887
36101
|
const uiGroupStateController = useUIGroupStateController(props, controlType, {
|
|
35888
36102
|
stateType,
|
|
35889
36103
|
childControlFilter,
|
|
@@ -35893,7 +36107,8 @@ const useControlgroupProps = (props, {
|
|
|
35893
36107
|
wantRequesterButtonState,
|
|
35894
36108
|
uiActionInternal,
|
|
35895
36109
|
allowCapture,
|
|
35896
|
-
cascadeValidationToChildren
|
|
36110
|
+
cascadeValidationToChildren,
|
|
36111
|
+
standalone
|
|
35897
36112
|
});
|
|
35898
36113
|
const [boundAction] = useActionBoundToOneParam(action, uiGroupStateController.uiStateSignal);
|
|
35899
36114
|
// Mirror single-input behaviour: a controlled value with nobody listening
|
|
@@ -40261,8 +40476,7 @@ const LinkPlain = props => {
|
|
|
40261
40476
|
selectionController
|
|
40262
40477
|
});
|
|
40263
40478
|
const [controlRootProps, controlHostProps] = useControlProps(props, {
|
|
40264
|
-
controlType: "link"
|
|
40265
|
-
allowNameless: true
|
|
40479
|
+
controlType: "link"
|
|
40266
40480
|
});
|
|
40267
40481
|
const {
|
|
40268
40482
|
basePseudoState
|
|
@@ -40694,6 +40908,10 @@ let navCount = 0;
|
|
|
40694
40908
|
// Worn by a nav of routes while a route movement between two of its tabs is
|
|
40695
40909
|
// pictured (see markIndicatorTakesPart, and the CSS below for what it decides).
|
|
40696
40910
|
const BETWEEN_TABS_ATTRIBUTE = "data-nav-between-tabs";
|
|
40911
|
+
// What one notch of a wheel that counts in LINES is worth on a row that counts
|
|
40912
|
+
// in pixels. A rough line, deliberately: nothing here needs to agree with a
|
|
40913
|
+
// paragraph, only to move the row about as far as a notch moves a page.
|
|
40914
|
+
const WHEEL_LINE_SIZE = 16;
|
|
40697
40915
|
const css$W = /* css */`@layer navi {
|
|
40698
40916
|
.navi_nav {
|
|
40699
40917
|
--nav-border: none;
|
|
@@ -40777,6 +40995,14 @@ const css$W = /* css */`@layer navi {
|
|
|
40777
40995
|
justify-content: stretch;
|
|
40778
40996
|
display: flex;
|
|
40779
40997
|
|
|
40998
|
+
&[data-scrollable] {
|
|
40999
|
+
max-width: 100%;
|
|
41000
|
+
}
|
|
41001
|
+
|
|
41002
|
+
&[data-scrollable][data-vertical] {
|
|
41003
|
+
max-height: 100%;
|
|
41004
|
+
}
|
|
41005
|
+
|
|
40780
41006
|
& .navi_link {
|
|
40781
41007
|
user-select: none;
|
|
40782
41008
|
--x-nav-child-border-radius: calc(var(--nav-border-radius) - var(--nav-padding));
|
|
@@ -40945,6 +41171,39 @@ const Nav = ({
|
|
|
40945
41171
|
const slideContainerElementRef = useRef(null);
|
|
40946
41172
|
const indicatorPosition = slideContainer ? positionOfCurrentIndicator(currentIndicator, vertical) : null;
|
|
40947
41173
|
|
|
41174
|
+
// Which tab of this row is the one being shown: the slide the container says
|
|
41175
|
+
// it is on, or, for a row of routes, the link the browser is on.
|
|
41176
|
+
const getCurrentTabElement = () => {
|
|
41177
|
+
const navElement = navRef.current;
|
|
41178
|
+
if (!navElement) {
|
|
41179
|
+
return null;
|
|
41180
|
+
}
|
|
41181
|
+
const containerElement = slideContainerElementRef.current;
|
|
41182
|
+
if (!containerElement) {
|
|
41183
|
+
return navElement.querySelector("[data-href-current]");
|
|
41184
|
+
}
|
|
41185
|
+
const currentArea = containerElement.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
|
|
41186
|
+
if (currentArea === null) {
|
|
41187
|
+
return null;
|
|
41188
|
+
}
|
|
41189
|
+
return navElement.querySelector(`[data-slide-target="${CSS.escape(currentArea)}"]`);
|
|
41190
|
+
};
|
|
41191
|
+
// …and which one the picture leans on while it is not on the current one:
|
|
41192
|
+
// the slide a finger is pulling in, the slide an animation is leaving. There
|
|
41193
|
+
// is one only while a travel is playing.
|
|
41194
|
+
const getTowardTabElement = () => {
|
|
41195
|
+
const navElement = navRef.current;
|
|
41196
|
+
const containerElement = slideContainerElementRef.current;
|
|
41197
|
+
if (!navElement || !containerElement) {
|
|
41198
|
+
return null;
|
|
41199
|
+
}
|
|
41200
|
+
const towardArea = containerElement.getAttribute(SLIDE_TOWARD_ATTRIBUTE);
|
|
41201
|
+
if (towardArea === null) {
|
|
41202
|
+
return null;
|
|
41203
|
+
}
|
|
41204
|
+
return navElement.querySelector(`[data-slide-target="${CSS.escape(towardArea)}"]`);
|
|
41205
|
+
};
|
|
41206
|
+
|
|
40948
41207
|
// Where the trait is and where it is headed, as four numbers of pixels the
|
|
40949
41208
|
// CSS above interpolates between (see the .navi_nav_indicator rules). Written
|
|
40950
41209
|
// by hand rather than rendered: it is read off the row as it stands, and the
|
|
@@ -40958,8 +41217,7 @@ const Nav = ({
|
|
|
40958
41217
|
}
|
|
40959
41218
|
const tabElements = Array.from(navElement.querySelectorAll("[data-slide-target]"));
|
|
40960
41219
|
const areaOf = tabElement => tabElement.getAttribute("data-slide-target");
|
|
40961
|
-
const
|
|
40962
|
-
const currentIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === currentArea);
|
|
41220
|
+
const currentIndex = tabElements.indexOf(getCurrentTabElement());
|
|
40963
41221
|
if (currentIndex === -1) {
|
|
40964
41222
|
// On a slide no tab in this row names: there is no tab to sit under.
|
|
40965
41223
|
navElement.removeAttribute("data-nav-indicator-measured");
|
|
@@ -41001,6 +41259,226 @@ const Nav = ({
|
|
|
41001
41259
|
// about the row as it is now.
|
|
41002
41260
|
const paintIndicatorGeometryRef = useRef(null);
|
|
41003
41261
|
paintIndicatorGeometryRef.current = paintIndicatorGeometry;
|
|
41262
|
+
|
|
41263
|
+
// A row of tabs wider than the box holding it scrolls (the caller asks for
|
|
41264
|
+
// that with overflowX) — and then saying which tab one is on is not enough,
|
|
41265
|
+
// that tab must be somewhere one can see. The trait needs nothing here: it
|
|
41266
|
+
// is placed with offsetLeft, inside the row, so it rides along.
|
|
41267
|
+
// The ROW is what gets scrolled, never the tab: element.scrollIntoView()
|
|
41268
|
+
// walks every scrolling ancestor it finds, and a tab row lives in sheets
|
|
41269
|
+
// that sit on pages that scroll — bringing a tab into sight must move the
|
|
41270
|
+
// row and nothing else.
|
|
41271
|
+
const tabInSightRef = useRef(null);
|
|
41272
|
+
const keepCurrentTabInSight = () => {
|
|
41273
|
+
const navElement = navRef.current;
|
|
41274
|
+
const currentTabElement = getCurrentTabElement();
|
|
41275
|
+
// Brought into sight once, and then left where it is: the row scrolls, so
|
|
41276
|
+
// the user may have pushed it elsewhere since to read the other tabs, and
|
|
41277
|
+
// a render that changed none of this has no business taking that back.
|
|
41278
|
+
if (currentTabElement === tabInSightRef.current) {
|
|
41279
|
+
return;
|
|
41280
|
+
}
|
|
41281
|
+
tabInSightRef.current = currentTabElement;
|
|
41282
|
+
if (!currentTabElement) {
|
|
41283
|
+
return;
|
|
41284
|
+
}
|
|
41285
|
+
scrollIntoViewScoped(currentTabElement, {
|
|
41286
|
+
container: navElement
|
|
41287
|
+
});
|
|
41288
|
+
};
|
|
41289
|
+
|
|
41290
|
+
// …and while a travel is playing the row gets there AT THE PACE OF THE
|
|
41291
|
+
// TRAVEL, off the very number the trait is drawn from: the container
|
|
41292
|
+
// publishes how far between the two slides the picture stands
|
|
41293
|
+
// (--slide-travel-progress, written on this row — see followerElements), and
|
|
41294
|
+
// the row is scrolled the same fraction of the way between the two tabs. The
|
|
41295
|
+
// slides gliding while the tabs jump at the end is one movement told twice.
|
|
41296
|
+
//
|
|
41297
|
+
// Read frame by frame, because that number is nobody's news: under a finger
|
|
41298
|
+
// the container writes it per frame with no announcement (a write is not a
|
|
41299
|
+
// change), and for a travel that was asked for it is the browser that
|
|
41300
|
+
// animates it. So the row samples it, exactly as the trait does — except that
|
|
41301
|
+
// a scroll offset is not a thing CSS can be handed.
|
|
41302
|
+
const travelScrollRef = useRef(null);
|
|
41303
|
+
const syncTabScroll = ({
|
|
41304
|
+
afterAFrame
|
|
41305
|
+
} = {}) => {
|
|
41306
|
+
const navElement = navRef.current;
|
|
41307
|
+
if (!navElement) {
|
|
41308
|
+
return;
|
|
41309
|
+
}
|
|
41310
|
+
const currentTabElement = getCurrentTabElement();
|
|
41311
|
+
const towardTabElement = getTowardTabElement();
|
|
41312
|
+
if (!currentTabElement || !towardTabElement) {
|
|
41313
|
+
travelScrollRef.current = null;
|
|
41314
|
+
// Nothing is travelling — but a travel that was ASKED for is announced a
|
|
41315
|
+
// breath after the slide it lands on, so a row that lands the moment the
|
|
41316
|
+
// current tab changes lands ahead of the movement it should have ridden,
|
|
41317
|
+
// and the slides then glide towards a row already there. Giving it a
|
|
41318
|
+
// frame costs nothing: the slides have not moved either.
|
|
41319
|
+
// Only for a row of slides, and only once — a row of routes has no travel
|
|
41320
|
+
// to ride, and the second look is the one that lands.
|
|
41321
|
+
if (slideContainer && !afterAFrame && tabInSightRef.current && tabInSightRef.current !== currentTabElement) {
|
|
41322
|
+
scheduleTabScrollSync({
|
|
41323
|
+
nextFrame: true
|
|
41324
|
+
});
|
|
41325
|
+
return;
|
|
41326
|
+
}
|
|
41327
|
+
// Nowhere between: the row lands where it belongs.
|
|
41328
|
+
keepCurrentTabInSight();
|
|
41329
|
+
return;
|
|
41330
|
+
}
|
|
41331
|
+
let travel = travelScrollRef.current;
|
|
41332
|
+
if (!travel || travel.from !== currentTabElement || travel.toward !== towardTabElement) {
|
|
41333
|
+
// The two ends, measured once for this travel: measuring them again on a
|
|
41334
|
+
// row that has since moved would be measuring from the answer.
|
|
41335
|
+
const fromOffsets = getScrollIntoViewScopedOffsets(currentTabElement, {
|
|
41336
|
+
container: navElement
|
|
41337
|
+
});
|
|
41338
|
+
const towardOffsets = getScrollIntoViewScopedOffsets(towardTabElement, {
|
|
41339
|
+
container: navElement
|
|
41340
|
+
});
|
|
41341
|
+
// Signed the way the container counts: +1 when the picture leans on a
|
|
41342
|
+
// slide sitting BEFORE the current one, -1 when it sits after — the same
|
|
41343
|
+
// reading paintIndicatorGeometry does of the same number.
|
|
41344
|
+
const sign = currentTabElement.compareDocumentPosition(towardTabElement) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
|
41345
|
+
travel = {
|
|
41346
|
+
from: currentTabElement,
|
|
41347
|
+
toward: towardTabElement,
|
|
41348
|
+
left: fromOffsets.left,
|
|
41349
|
+
top: fromOffsets.top,
|
|
41350
|
+
leftDelta: (towardOffsets.left - fromOffsets.left) * sign,
|
|
41351
|
+
topDelta: (towardOffsets.top - fromOffsets.top) * sign
|
|
41352
|
+
};
|
|
41353
|
+
travelScrollRef.current = travel;
|
|
41354
|
+
}
|
|
41355
|
+
const progress = parseFloat(getComputedStyle(navElement).getPropertyValue("--slide-travel-progress")) || 0;
|
|
41356
|
+
navElement.scrollTo({
|
|
41357
|
+
left: travel.left + progress * travel.leftDelta,
|
|
41358
|
+
top: travel.top + progress * travel.topDelta,
|
|
41359
|
+
// The row is being driven frame by frame: a caller who asked for smooth
|
|
41360
|
+
// scrolling asked it of a scroll that lands, not of one that is held.
|
|
41361
|
+
behavior: "instant"
|
|
41362
|
+
});
|
|
41363
|
+
scheduleTabScrollSync({
|
|
41364
|
+
nextFrame: true
|
|
41365
|
+
});
|
|
41366
|
+
};
|
|
41367
|
+
const syncTabScrollRef = useRef(null);
|
|
41368
|
+
syncTabScrollRef.current = syncTabScroll;
|
|
41369
|
+
|
|
41370
|
+
// When to look: on the next frame while a travel is being followed, and
|
|
41371
|
+
// otherwise at the end of the current task. A MICROTASK and not a frame,
|
|
41372
|
+
// because the two are told apart in the same breath — a travel that was
|
|
41373
|
+
// asked for announces the slide it lands on before it announces the one it
|
|
41374
|
+
// is leaving, so a row settling straight away would jump to the destination
|
|
41375
|
+
// and only then be asked to travel there. And it is still before the paint,
|
|
41376
|
+
// which a frame is not: a row that opens on a far tab must be scrolled
|
|
41377
|
+
// already the first time it is seen.
|
|
41378
|
+
const syncScheduledRef = useRef(false);
|
|
41379
|
+
const syncFrameRef = useRef(0);
|
|
41380
|
+
const scheduleTabScrollSync = ({
|
|
41381
|
+
nextFrame
|
|
41382
|
+
} = {}) => {
|
|
41383
|
+
if (syncScheduledRef.current) {
|
|
41384
|
+
return;
|
|
41385
|
+
}
|
|
41386
|
+
syncScheduledRef.current = true;
|
|
41387
|
+
const run = () => {
|
|
41388
|
+
syncScheduledRef.current = false;
|
|
41389
|
+
syncFrameRef.current = 0;
|
|
41390
|
+
syncTabScrollRef.current({
|
|
41391
|
+
afterAFrame: nextFrame
|
|
41392
|
+
});
|
|
41393
|
+
};
|
|
41394
|
+
if (nextFrame) {
|
|
41395
|
+
syncFrameRef.current = requestAnimationFrame(run);
|
|
41396
|
+
} else {
|
|
41397
|
+
queueMicrotask(run);
|
|
41398
|
+
}
|
|
41399
|
+
};
|
|
41400
|
+
const scheduleTabScrollSyncRef = useRef(null);
|
|
41401
|
+
scheduleTabScrollSyncRef.current = scheduleTabScrollSync;
|
|
41402
|
+
useLayoutEffect(() => {
|
|
41403
|
+
return () => {
|
|
41404
|
+
cancelAnimationFrame(syncFrameRef.current);
|
|
41405
|
+
};
|
|
41406
|
+
}, []);
|
|
41407
|
+
|
|
41408
|
+
// The row watches its own size and reads itself again when it changes — a
|
|
41409
|
+
// badge count, a font that just arrived, a window resized: what was written
|
|
41410
|
+
// is pixels, where the trait sits and how far the row is scrolled, and
|
|
41411
|
+
// pixels measured on a row of another width are wrong. A row that had no
|
|
41412
|
+
// width at all is the same story with a sharper edge: inside a closed
|
|
41413
|
+
// <dialog> everything measures 0, and the current tab does not change when
|
|
41414
|
+
// the dialog opens, so nothing else would ever look again.
|
|
41415
|
+
// A scroll takes no size from anything, so this stays quiet while the row is
|
|
41416
|
+
// being pushed around by hand.
|
|
41417
|
+
useLayoutEffect(() => {
|
|
41418
|
+
const sizeObserver = new ResizeObserver(() => {
|
|
41419
|
+
paintIndicatorGeometryRef.current();
|
|
41420
|
+
// What was brought into sight was brought there for another row.
|
|
41421
|
+
tabInSightRef.current = null;
|
|
41422
|
+
scheduleTabScrollSyncRef.current();
|
|
41423
|
+
});
|
|
41424
|
+
sizeObserver.observe(navRef.current);
|
|
41425
|
+
return () => {
|
|
41426
|
+
sizeObserver.disconnect();
|
|
41427
|
+
};
|
|
41428
|
+
}, []);
|
|
41429
|
+
|
|
41430
|
+
// A row that scrolls sideways, and a mouse that only turns one way. A
|
|
41431
|
+
// vertical wheel is left to the vertical axis, which a row of tabs does not
|
|
41432
|
+
// have: the push goes past the row to the page, and the tabs stay where they
|
|
41433
|
+
// were — a row a trackpad can push around and a mouse cannot reach at all.
|
|
41434
|
+
// Up and further along are the same wish here, so the push moves the row.
|
|
41435
|
+
// Only for a row that scrolls on ONE axis: one that scrolls the way the
|
|
41436
|
+
// wheel turns already has the answer it wants, and taking the push would be
|
|
41437
|
+
// answering it twice.
|
|
41438
|
+
useLayoutEffect(() => {
|
|
41439
|
+
const navElement = navRef.current;
|
|
41440
|
+
const onWheel = wheelEvent => {
|
|
41441
|
+
// The burst is already somebody else's — a box travelling under the
|
|
41442
|
+
// pointer, a wheel picker — and stays theirs until it dies out, wherever
|
|
41443
|
+
// the pointer has drifted since (see wheel_gesture.js).
|
|
41444
|
+
if (wheelGestureIsTakenFrom(navElement)) {
|
|
41445
|
+
return;
|
|
41446
|
+
}
|
|
41447
|
+
const {
|
|
41448
|
+
deltaX,
|
|
41449
|
+
deltaY
|
|
41450
|
+
} = wheelEvent;
|
|
41451
|
+
if (Math.abs(deltaY) <= Math.abs(deltaX)) {
|
|
41452
|
+
// Sideways already: the browser knows where to put it.
|
|
41453
|
+
return;
|
|
41454
|
+
}
|
|
41455
|
+
const scrollableWidth = navElement.scrollWidth - navElement.clientWidth;
|
|
41456
|
+
const scrollableHeight = navElement.scrollHeight - navElement.clientHeight;
|
|
41457
|
+
if (scrollableWidth <= 0 || scrollableHeight > 0) {
|
|
41458
|
+
return;
|
|
41459
|
+
}
|
|
41460
|
+
// Not every wheel speaks in pixels: some count lines, some count pages.
|
|
41461
|
+
const notch = wheelEvent.deltaMode === 1 ? WHEEL_LINE_SIZE : wheelEvent.deltaMode === 2 ? navElement.clientWidth : 1;
|
|
41462
|
+
const scrollLeftBefore = navElement.scrollLeft;
|
|
41463
|
+
const scrollLeftWanted = Math.max(0, Math.min(scrollableWidth, scrollLeftBefore + deltaY * notch));
|
|
41464
|
+
if (scrollLeftWanted === scrollLeftBefore) {
|
|
41465
|
+
// The row is at its end and the hand is still pushing: what is left
|
|
41466
|
+
// over is the page's, exactly as it would have been without any of
|
|
41467
|
+
// this.
|
|
41468
|
+
return;
|
|
41469
|
+
}
|
|
41470
|
+
wheelEvent.preventDefault();
|
|
41471
|
+
claimWheelGesture(navElement);
|
|
41472
|
+
navElement.scrollLeft = scrollLeftWanted;
|
|
41473
|
+
};
|
|
41474
|
+
navElement.addEventListener("wheel", onWheel, {
|
|
41475
|
+
passive: false
|
|
41476
|
+
});
|
|
41477
|
+
return () => {
|
|
41478
|
+
navElement.removeEventListener("wheel", onWheel);
|
|
41479
|
+
releaseWheelGesture(navElement);
|
|
41480
|
+
};
|
|
41481
|
+
}, []);
|
|
41004
41482
|
useLayoutEffect(() => {
|
|
41005
41483
|
if (!slideContainer) {
|
|
41006
41484
|
return undefined;
|
|
@@ -41014,6 +41492,7 @@ const Nav = ({
|
|
|
41014
41492
|
const readContainer = () => {
|
|
41015
41493
|
setCurrentSlideArea(containerElement.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ?? undefined);
|
|
41016
41494
|
paintIndicatorGeometryRef.current();
|
|
41495
|
+
scheduleTabScrollSyncRef.current();
|
|
41017
41496
|
};
|
|
41018
41497
|
readContainer();
|
|
41019
41498
|
// Where one is and what the picture leans on are written on the container,
|
|
@@ -41023,16 +41502,8 @@ const Nav = ({
|
|
|
41023
41502
|
// because a write is not a change — the attribute the trait follows is
|
|
41024
41503
|
// written on every frame of a gesture with the same value in it.
|
|
41025
41504
|
containerElement.addEventListener(SLIDE_STATE_EVENT, readContainer);
|
|
41026
|
-
// A row whose tabs changed width — a badge count, a font that just
|
|
41027
|
-
// arrived, a window resized — is measured again: what was written is
|
|
41028
|
-
// pixels, and pixels go stale.
|
|
41029
|
-
const sizeObserver = new ResizeObserver(() => {
|
|
41030
|
-
paintIndicatorGeometryRef.current();
|
|
41031
|
-
});
|
|
41032
|
-
sizeObserver.observe(navRef.current);
|
|
41033
41505
|
return () => {
|
|
41034
41506
|
containerElement.removeEventListener(SLIDE_STATE_EVENT, readContainer);
|
|
41035
|
-
sizeObserver.disconnect();
|
|
41036
41507
|
slideContainerElementRef.current = null;
|
|
41037
41508
|
};
|
|
41038
41509
|
}, [slideContainer]);
|
|
@@ -41041,6 +41512,7 @@ const Nav = ({
|
|
|
41041
41512
|
// and no observer above watches this row's own children.
|
|
41042
41513
|
useLayoutEffect(() => {
|
|
41043
41514
|
paintIndicatorGeometry();
|
|
41515
|
+
scheduleTabScrollSync();
|
|
41044
41516
|
});
|
|
41045
41517
|
|
|
41046
41518
|
// Whether the bar keeps its name for the route movement about to be pictured
|
|
@@ -43708,7 +44180,7 @@ const Expandable = props => {
|
|
|
43708
44180
|
}
|
|
43709
44181
|
if (hasAction) {
|
|
43710
44182
|
if (nextOpen) {
|
|
43711
|
-
effectiveAction.run();
|
|
44183
|
+
runUnwatched(() => effectiveAction.run());
|
|
43712
44184
|
} else {
|
|
43713
44185
|
effectiveAction.abort();
|
|
43714
44186
|
}
|
|
@@ -43859,7 +44331,7 @@ const Expandable = props => {
|
|
|
43859
44331
|
// Mounted already open: the content is visible, its data is due.
|
|
43860
44332
|
useEffect(() => {
|
|
43861
44333
|
if (openedRef.current && hasAction) {
|
|
43862
|
-
effectiveAction.run();
|
|
44334
|
+
runUnwatched(() => effectiveAction.run());
|
|
43863
44335
|
}
|
|
43864
44336
|
}, []);
|
|
43865
44337
|
const onRootKeyDown = keyboardEvent => {
|
|
@@ -44675,8 +45147,7 @@ const ButtonUI = props => {
|
|
|
44675
45147
|
loadingOutline = true
|
|
44676
45148
|
} = props;
|
|
44677
45149
|
const [buttonControlRootProps, buttonControlHostProps, controlChildrenWrapperProps] = useControlProps(props, {
|
|
44678
|
-
controlType: "button"
|
|
44679
|
-
allowNameless: true
|
|
45150
|
+
controlType: "button"
|
|
44680
45151
|
});
|
|
44681
45152
|
const {
|
|
44682
45153
|
basePseudoState,
|
|
@@ -49740,30 +50211,18 @@ const Form = props => {
|
|
|
49740
50211
|
// is a different component: same group, no <form> element and none of the
|
|
49741
50212
|
// browser machinery that comes with it.
|
|
49742
50213
|
const isNested = Boolean(useContext(FormContext));
|
|
49743
|
-
|
|
50214
|
+
return isNested ? jsx(FormNested, {
|
|
49744
50215
|
...props
|
|
49745
50216
|
}) : jsx(FormControl, {
|
|
49746
50217
|
...props
|
|
49747
50218
|
});
|
|
49748
|
-
if (props.standalone) {
|
|
49749
|
-
// Nothing above to register with: the group hooks read the parent from
|
|
49750
|
-
// this context, so emptying it here is the whole opt-out.
|
|
49751
|
-
return jsx(ParentUIStateControllerContext.Provider, {
|
|
49752
|
-
value: undefined,
|
|
49753
|
-
children: form
|
|
49754
|
-
});
|
|
49755
|
-
}
|
|
49756
|
-
return form;
|
|
49757
50219
|
};
|
|
49758
50220
|
|
|
49759
50221
|
// What both forms are made of: one group, one context for what is inside it.
|
|
49760
|
-
// standalone is read by Form above and never goes further — least of all to the
|
|
49761
|
-
// DOM.
|
|
49762
50222
|
const useFormGroup = props => {
|
|
49763
50223
|
const propsForGroup = {
|
|
49764
50224
|
...props
|
|
49765
50225
|
};
|
|
49766
|
-
delete propsForGroup.standalone;
|
|
49767
50226
|
delete propsForGroup.canSendWhileUnchanged;
|
|
49768
50227
|
delete propsForGroup.pristineKey;
|
|
49769
50228
|
// Not the generic control `command`, which a control triggers on its own ui
|
|
@@ -58653,9 +59112,16 @@ const PickerCustomResolver = props => {
|
|
|
58653
59112
|
if (circle) {
|
|
58654
59113
|
props.variant = "icon";
|
|
58655
59114
|
}
|
|
58656
|
-
// A door, never a field (see `
|
|
58657
|
-
//
|
|
58658
|
-
|
|
59115
|
+
// A door, never a field (see `standalone`): the form around it expects no
|
|
59116
|
+
// value from it. So the trigger answers to "button" — it is pressed to read
|
|
59117
|
+
// something, never typed into — the same thing a confirm picker says
|
|
59118
|
+
// through `picksNothing` (see picker.jsx). Said as the role here rather
|
|
59119
|
+
// than through that prop: `picksNothing` also takes the right slot away,
|
|
59120
|
+
// and the status icon a callout trigger draws sits in it.
|
|
59121
|
+
props.standalone = true;
|
|
59122
|
+
if (props.role === undefined) {
|
|
59123
|
+
props.role = "button";
|
|
59124
|
+
}
|
|
58659
59125
|
// A word in a sentence asks for a plain tooltip — no icon in the callout,
|
|
58660
59126
|
// no status color; an icon one presses is the callout's own status icon,
|
|
58661
59127
|
// and says "info" like the callout it opens.
|
|
@@ -59494,7 +59960,8 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
59494
59960
|
*
|
|
59495
59961
|
* Yes is said with `--navi-confirm` (see commands.js). The popup closes on it,
|
|
59496
59962
|
* and only then does the picker do what it was standing in for — run its
|
|
59497
|
-
* `action`,
|
|
59963
|
+
* `action`, trigger its `command`, or both, the command following the action
|
|
59964
|
+
* and only if it succeeded — so the work runs on the trigger, where
|
|
59498
59965
|
* the loading state and the error callout are drawn, and where the user is
|
|
59499
59966
|
* looking again. Anything else that closes the popup (the cancel button,
|
|
59500
59967
|
* Escape, a click outside) is no.
|
|
@@ -59556,21 +60023,31 @@ const PickerConfirmResolver = props => {
|
|
|
59556
60023
|
// trigger.
|
|
59557
60024
|
const onConfirm = confirmEvent => {
|
|
59558
60025
|
const inputEl = getPickerInput(ref.current);
|
|
59559
|
-
|
|
59560
|
-
|
|
59561
|
-
|
|
59562
|
-
|
|
59563
|
-
|
|
60026
|
+
const runCommand = () => {
|
|
60027
|
+
triggerNaviCommand(inputEl, command, confirmEvent);
|
|
60028
|
+
};
|
|
60029
|
+
if (action === undefined) {
|
|
60030
|
+
if (command) {
|
|
60031
|
+
runCommand();
|
|
60032
|
+
}
|
|
59564
60033
|
return;
|
|
59565
60034
|
}
|
|
60035
|
+
// Both props on one trigger means "do this, then go there": the action runs
|
|
60036
|
+
// and the command follows it, only if it worked — a delete that fails must
|
|
60037
|
+
// not send the user to the list it did not leave. Same rule a `<Button
|
|
60038
|
+
// action command>` has (see onButtonInteractionAllowed in control_hooks).
|
|
60039
|
+
const completion = watchActionCompletion(findControlHost(inputEl) || inputEl, () => dispatchRequestAction(inputEl, {
|
|
60040
|
+
event: confirmEvent,
|
|
60041
|
+
name: "confirm"
|
|
60042
|
+
}));
|
|
59566
60043
|
if (command) {
|
|
59567
|
-
|
|
60044
|
+
runWhenActionSucceeded(completion, runCommand);
|
|
59568
60045
|
}
|
|
59569
60046
|
};
|
|
59570
60047
|
return jsx(Next, {
|
|
59571
60048
|
...props,
|
|
59572
60049
|
type: "navi_js",
|
|
59573
|
-
|
|
60050
|
+
standalone: true
|
|
59574
60051
|
// No chevron, no clear cross, whatever variant the trigger is drawn in:
|
|
59575
60052
|
// both announce a value, and a question holds none.
|
|
59576
60053
|
,
|
|
@@ -61970,6 +62447,7 @@ const ListFirstResolver = props => {
|
|
|
61970
62447
|
* deselectable?: boolean,
|
|
61971
62448
|
* maxLength?: number,
|
|
61972
62449
|
* maxLengthGuard?: number,
|
|
62450
|
+
* standalone?: boolean,
|
|
61973
62451
|
* action?: (value: any) => void,
|
|
61974
62452
|
* uiAction?: (value: any) => void,
|
|
61975
62453
|
* popover?: boolean,
|
|
@@ -62145,6 +62623,12 @@ const ListFirstResolver = props => {
|
|
|
62145
62623
|
* taking — and `uiAction` is not called. The selected ones stay takeable
|
|
62146
62624
|
* back, so a selection that arrived too long can always be brought back
|
|
62147
62625
|
* under the limit. Implies `maxLength` for validity.
|
|
62626
|
+
* @param {boolean} [props.standalone]
|
|
62627
|
+
* This list answers for itself: it does not register with the control group
|
|
62628
|
+
* or picker around it, so its selection stays out of that value and nothing
|
|
62629
|
+
* coming down — a distributed value, a reset — reaches it. What a popup whose
|
|
62630
|
+
* one answer is spread over several lists says, so that none of them is taken
|
|
62631
|
+
* for the answer itself.
|
|
62148
62632
|
*/
|
|
62149
62633
|
const List = createComponentResolver([ListFirstResolver, ListSelectableResolver, ListUI]);
|
|
62150
62634
|
const ListContent = ({
|
|
@@ -65911,8 +66395,7 @@ const BadgeButtonUI = props => {
|
|
|
65911
66395
|
const defaultRef = useRef();
|
|
65912
66396
|
props.ref = props.ref || defaultRef;
|
|
65913
66397
|
const [buttonRootProps, buttonHostProps] = useControlProps(props, {
|
|
65914
|
-
controlType: "button"
|
|
65915
|
-
allowNameless: true
|
|
66398
|
+
controlType: "button"
|
|
65916
66399
|
});
|
|
65917
66400
|
return jsx(Text, {
|
|
65918
66401
|
className: "navi_badge_button",
|
|
@@ -67364,6 +67847,11 @@ const PickerButton = props => {
|
|
|
67364
67847
|
// confirm resolver (see picker_confirm.jsx), which is the picker that is
|
|
67365
67848
|
// one — a caller whose popup is a menu of actions says it for themselves.
|
|
67366
67849
|
picksNothing,
|
|
67850
|
+
// Where the caller's own role goes: onto the host, beside the name (see
|
|
67851
|
+
// control_context.js) — a role and a name on two different elements name
|
|
67852
|
+
// nothing at all. Left unsaid, a picker that picks nothing takes "button"
|
|
67853
|
+
// (see PickerInput below).
|
|
67854
|
+
role,
|
|
67367
67855
|
readOnly,
|
|
67368
67856
|
error
|
|
67369
67857
|
} = props;
|
|
@@ -67418,6 +67906,21 @@ const PickerButton = props => {
|
|
|
67418
67906
|
// clearing being a modification like any other.
|
|
67419
67907
|
const interactive = !basePseudoState[":disabled"] && !basePseudoState[":read-only"] && !loading;
|
|
67420
67908
|
usePickerErrorCallout(uiStateController, error);
|
|
67909
|
+
// What the picker really is, said on the element that holds the name and
|
|
67910
|
+
// takes the focus (the host input below): a picker that picks nothing is a
|
|
67911
|
+
// door — its popup is a question (type="confirm") or something to read
|
|
67912
|
+
// (mode="callout", which says it for itself) — and a door is pressed. So it
|
|
67913
|
+
// answers to "button" rather than to the "textbox" its <input> would
|
|
67914
|
+
// otherwise be, which is both what a screen reader announces and what
|
|
67915
|
+
// getByRole("button", { name }) finds. A picker holding a value stays a
|
|
67916
|
+
// textbox: it does hold text.
|
|
67917
|
+
const hostRole = role ?? (picksNothing ? "button" : undefined);
|
|
67918
|
+
// And what that button is called, when the caller wrote it as a drawing
|
|
67919
|
+
// rather than as a word: a door draws no value, so what sits in its value
|
|
67920
|
+
// slot IS its label — the `ui` a caller gave it, or its placeholder. Skipped
|
|
67921
|
+
// the moment the caller names it themselves (aria-label/aria-labelledby),
|
|
67922
|
+
// which is the only way to name a door drawn as an icon.
|
|
67923
|
+
const hostLabelId = hostRole === "button" && inputProps["aria-label"] === undefined && inputProps["aria-labelledby"] === undefined && variant !== "headless" && ui !== "default" ? `${inputProps.id}_picker_label` : undefined;
|
|
67421
67924
|
// What the picker knows about itself, for the pieces it does not place: the
|
|
67422
67925
|
// drawings of its value (Picker.UI.*), and the affordances a caller may put
|
|
67423
67926
|
// in their own `ui` (Picker.Clear) as much as the ones it puts in its slot.
|
|
@@ -67464,6 +67967,7 @@ const PickerButton = props => {
|
|
|
67464
67967
|
basePseudoState: basePseudoState,
|
|
67465
67968
|
styleCSSVars: PickerStyleCSSVars,
|
|
67466
67969
|
variant: undefined,
|
|
67970
|
+
role: undefined,
|
|
67467
67971
|
rightSlotIcon: undefined,
|
|
67468
67972
|
rightSlotIconSize: undefined,
|
|
67469
67973
|
rightSlot: undefined,
|
|
@@ -67512,6 +68016,8 @@ const PickerButton = props => {
|
|
|
67512
68016
|
color: "var(--picker-loader-color)",
|
|
67513
68017
|
inset: -2
|
|
67514
68018
|
}), jsx(PickerInput, {
|
|
68019
|
+
role: hostRole,
|
|
68020
|
+
"aria-labelledby": hostLabelId,
|
|
67515
68021
|
tabIndex: variant === "headless" ? -1 : undefined,
|
|
67516
68022
|
"aria-hidden": variant === "headless" ? "true" : undefined,
|
|
67517
68023
|
...inputProps,
|
|
@@ -67587,6 +68093,10 @@ const PickerButton = props => {
|
|
|
67587
68093
|
}
|
|
67588
68094
|
}), variant === "headless" || ui === "default" ? null : jsx(Text, {
|
|
67589
68095
|
className: "navi_picker_value"
|
|
68096
|
+
// Only when it names the trigger (see hostLabelId above).
|
|
68097
|
+
,
|
|
68098
|
+
|
|
68099
|
+
id: hostLabelId
|
|
67590
68100
|
// Tells the caller's own drawing of the control from the value
|
|
67591
68101
|
// the picker draws itself, so each is written on its own line
|
|
67592
68102
|
// (see .navi_picker_value in the CSS above).
|
|
@@ -70549,7 +71059,7 @@ const SplitButton = props => {
|
|
|
70549
71059
|
}), jsx(Picker, {
|
|
70550
71060
|
id: menuId,
|
|
70551
71061
|
variant: "headless",
|
|
70552
|
-
|
|
71062
|
+
standalone: true,
|
|
70553
71063
|
anchor: rootRef,
|
|
70554
71064
|
...popupProps,
|
|
70555
71065
|
readOnly: readOnly,
|