@jsenv/navi 0.29.141 → 0.29.143
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 +282 -164
- package/dist/jsenv_navi.js.map +12 -20
- package/docs/AI_INSTRUCTIONS.md +17 -3
- package/docs/actions.md +3 -0
- package/docs/control_value.md +7 -1
- package/docs/css_architecture.md +1 -1
- package/docs/navigation.md +117 -35
- package/docs/popup_open.md +18 -5
- package/docs/state_binding.md +142 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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
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, initFocusGroup, scrollIntoViewScoped, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
8
|
-
export { clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
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";
|
|
11
11
|
import { useErrorBoundary, useLayoutEffect, useContext, useCallback, useRef, useState, useEffect, useMemo, useId } from "preact/hooks";
|
|
@@ -16349,6 +16349,14 @@ const generateSignalId = () => {
|
|
|
16349
16349
|
* value, and it goes back to the default value when the route stops matching. Use it for params like an
|
|
16350
16350
|
* "edit this one" id, where every other link to the screen must lead to the plain screen.
|
|
16351
16351
|
* Incompatible with `persists` (throws).
|
|
16352
|
+
* @param {"replace"|"push"} [options.history="replace"] - what writing this state
|
|
16353
|
+
* is worth in the browser's history, when it is bound to a url (a route's
|
|
16354
|
+
* `searchParams`, a path param). Replacement by default: a state that
|
|
16355
|
+
* qualifies the screen one is on — a step in a form, a view mode, a sort — is
|
|
16356
|
+
* not a place, and one entry per write turns a single back-press into as many
|
|
16357
|
+
* as the user moved. `"push"` is for a state whose values ARE places one came
|
|
16358
|
+
* from (the photo one is looking at in a gallery). Whatever is said here, one
|
|
16359
|
+
* write can say otherwise: `signal.set(value, { history })`.
|
|
16352
16360
|
* @param {boolean} [options.debug=false] - Enable debug logging for this signal's operations
|
|
16353
16361
|
* @returns {import("@preact/signals").Signal} A signal that can be synchronized with a source signal and/or persisted in localStorage. The signal includes a `validity` property for validation state.
|
|
16354
16362
|
*
|
|
@@ -16422,8 +16430,14 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
16422
16430
|
ignoreArrayOrder,
|
|
16423
16431
|
autoFix,
|
|
16424
16432
|
weak = false,
|
|
16433
|
+
history = "replace",
|
|
16425
16434
|
} = options;
|
|
16426
16435
|
|
|
16436
|
+
if (history !== "replace" && history !== "push") {
|
|
16437
|
+
throw new TypeError(
|
|
16438
|
+
`stateSignal "${id}": history must be "replace" or "push", got ${history}.`,
|
|
16439
|
+
);
|
|
16440
|
+
}
|
|
16427
16441
|
if (weak && persists) {
|
|
16428
16442
|
throw new TypeError(
|
|
16429
16443
|
`stateSignal "${id}": weak and persists are contradictory — a weak param qualifies one visit, it cannot be restored from a previous session.`,
|
|
@@ -16600,7 +16614,35 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
16600
16614
|
configurable: true,
|
|
16601
16615
|
});
|
|
16602
16616
|
|
|
16617
|
+
// What the write being made right now asks of the history, over what this
|
|
16618
|
+
// state declares. It lives for the length of one assignment: the effect that
|
|
16619
|
+
// writes the url runs synchronously inside it (see route.js), and reads it
|
|
16620
|
+
// there. A write made inside batch() is therefore out of its reach — say the
|
|
16621
|
+
// history on the state itself for those.
|
|
16622
|
+
let historyForCurrentWrite;
|
|
16603
16623
|
const facadeSignal = preactSignal;
|
|
16624
|
+
/**
|
|
16625
|
+
* Write the state, saying what this ONE write is worth in the history.
|
|
16626
|
+
*
|
|
16627
|
+
* The state says which of the two it usually is (`history` above); a writer
|
|
16628
|
+
* that knows THIS move is not one of them says so here — a slide reached by
|
|
16629
|
+
* dragging inside a container whose slides are places one came from.
|
|
16630
|
+
*
|
|
16631
|
+
* @param {any} value
|
|
16632
|
+
* @param {{history?: "replace"|"push"}} [options]
|
|
16633
|
+
*/
|
|
16634
|
+
facadeSignal.set = (value, { history: historyAsked } = {}) => {
|
|
16635
|
+
if (historyAsked === undefined) {
|
|
16636
|
+
facadeSignal.value = value;
|
|
16637
|
+
return;
|
|
16638
|
+
}
|
|
16639
|
+
historyForCurrentWrite = historyAsked;
|
|
16640
|
+
try {
|
|
16641
|
+
facadeSignal.value = value;
|
|
16642
|
+
} finally {
|
|
16643
|
+
historyForCurrentWrite = undefined;
|
|
16644
|
+
}
|
|
16645
|
+
};
|
|
16604
16646
|
facadeSignal.validity = validity;
|
|
16605
16647
|
facadeSignal.validSignal = computed(() => {
|
|
16606
16648
|
// Reading facadeSignal.value establishes the reactive dependency.
|
|
@@ -16745,6 +16787,8 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
16745
16787
|
localStorageKey,
|
|
16746
16788
|
debug,
|
|
16747
16789
|
...options,
|
|
16790
|
+
history,
|
|
16791
|
+
getHistory: () => historyForCurrentWrite || history,
|
|
16748
16792
|
};
|
|
16749
16793
|
globalSignalRegistry.set(signalIdString, {
|
|
16750
16794
|
signal: facadeSignal,
|
|
@@ -18012,9 +18056,20 @@ const POSITION_PROPS = {
|
|
|
18012
18056
|
return { transform: `skew(${value})` };
|
|
18013
18057
|
},
|
|
18014
18058
|
};
|
|
18059
|
+
// Only the inline axis has something to truncate here, so the clip must not
|
|
18060
|
+
// constrain the block one: `hidden` would make the element a scroll container,
|
|
18061
|
+
// whose flex automatic minimum size is 0, and a single line inside a column
|
|
18062
|
+
// flex container shorter than 1lh then shrinks under its own line box and gets
|
|
18063
|
+
// cut through the glyphs. `clip` keeps the content-based minimum, so the line
|
|
18064
|
+
// overflows visibly instead. The margin leaves room for ink drawn outside the
|
|
18065
|
+
// advance width; it starts from the padding box, having no hidden line to hold
|
|
18066
|
+
// back the way lineClampStyles below does.
|
|
18015
18067
|
const singleLineEllipsisStyles = () => {
|
|
18016
18068
|
return {
|
|
18017
|
-
overflow: "
|
|
18069
|
+
overflow: "clip",
|
|
18070
|
+
// The `padding-box` keyword is redundant (it is the default box) but
|
|
18071
|
+
// Chromium drops the declaration when a bare calc() follows the property.
|
|
18072
|
+
overflowClipMargin: "padding-box calc((1lh - 1em) / 2)",
|
|
18018
18073
|
textOverflow: "ellipsis",
|
|
18019
18074
|
overflowWrap: "normal",
|
|
18020
18075
|
};
|
|
@@ -24540,7 +24595,15 @@ let isUpdatingRoutesFromUrl = false;
|
|
|
24540
24595
|
* `:name=${signal}` binds that param to a signal.
|
|
24541
24596
|
* @param {object} [options]
|
|
24542
24597
|
* @param {Object<string, import("@preact/signals").Signal>} [options.searchParams]
|
|
24543
|
-
* Search params this route two-way syncs with, by name.
|
|
24598
|
+
* Search params this route two-way syncs with, by name. The signal and the url
|
|
24599
|
+
* are the same state: writing the signal rewrites the address, an address
|
|
24600
|
+
* arriving from outside writes the signal, and the param disappears from the
|
|
24601
|
+
* url while the signal holds its default.
|
|
24602
|
+
* Writing one AMENDS the history entry one is on — a param qualifies the
|
|
24603
|
+
* screen, it is not a place, and one entry per write turns a single
|
|
24604
|
+
* back-press into as many as the user moved. A state whose values ARE places
|
|
24605
|
+
* says so where it is declared (`stateSignal(v, { history: "push" })`), and
|
|
24606
|
+
* one write can still say otherwise (`signal.set(v, { history })`).
|
|
24544
24607
|
* @param {Object<string, RegExp | Array | ((value: string) => boolean)>} [options.params]
|
|
24545
24608
|
* Which values a path param accepts, by param name: a regexp tested against the
|
|
24546
24609
|
* decoded segment, the list of accepted values (compared as strings, so it can
|
|
@@ -24711,17 +24774,20 @@ const route = (
|
|
|
24711
24774
|
const routeUrl = route.buildUrl(params);
|
|
24712
24775
|
return integration.navTo(routeUrl, options);
|
|
24713
24776
|
};
|
|
24714
|
-
route.redirectTo = (params, { callReason } = {}) => {
|
|
24777
|
+
route.redirectTo = (params, { callReason, history = "replace" } = {}) => {
|
|
24715
24778
|
if (!integration) {
|
|
24716
24779
|
return Promise.resolve();
|
|
24717
24780
|
}
|
|
24718
24781
|
const routeUrl = route.buildUrl(params);
|
|
24719
24782
|
return integration.navTo(routeUrl, {
|
|
24720
|
-
replace:
|
|
24783
|
+
replace: history !== "push",
|
|
24721
24784
|
callReason,
|
|
24722
24785
|
});
|
|
24723
24786
|
};
|
|
24724
|
-
route.replaceParams = (
|
|
24787
|
+
route.replaceParams = (
|
|
24788
|
+
newParams,
|
|
24789
|
+
{ callReason, isSignalChange, history = "replace" } = {},
|
|
24790
|
+
) => {
|
|
24725
24791
|
const matching = route.matchingSignal.peek();
|
|
24726
24792
|
if (!matching) {
|
|
24727
24793
|
console.warn(
|
|
@@ -24774,6 +24840,7 @@ const route = (
|
|
|
24774
24840
|
}
|
|
24775
24841
|
return mostSpecificRoute.redirectTo(newParams, {
|
|
24776
24842
|
callReason: `replaceParams delegation from ${route} to ${mostSpecificRoute} (original reason: ${callReason})`,
|
|
24843
|
+
history,
|
|
24777
24844
|
});
|
|
24778
24845
|
}
|
|
24779
24846
|
|
|
@@ -24789,7 +24856,7 @@ const route = (
|
|
|
24789
24856
|
newParams,
|
|
24790
24857
|
);
|
|
24791
24858
|
return integration.navTo(targetUrl, {
|
|
24792
|
-
replace:
|
|
24859
|
+
replace: history !== "push",
|
|
24793
24860
|
callReason,
|
|
24794
24861
|
});
|
|
24795
24862
|
};
|
|
@@ -24860,6 +24927,13 @@ const route = (
|
|
|
24860
24927
|
const { connections } = routePattern;
|
|
24861
24928
|
for (const connection of connections) {
|
|
24862
24929
|
const { signal: paramSignal, debug, paramName } = connection;
|
|
24930
|
+
// What a write of this state is worth in the history: what the state
|
|
24931
|
+
// declares, and what the write happening right now says instead (see
|
|
24932
|
+
// stateSignal's `history` and its `set`). Read at the moment of the
|
|
24933
|
+
// navigation and never before. A plain signal says nothing and replaces,
|
|
24934
|
+
// which is what a param qualifying a screen is.
|
|
24935
|
+
const historyOfWrite = () =>
|
|
24936
|
+
paramSignal.options?.getHistory?.() || "replace";
|
|
24863
24937
|
if (debug) {
|
|
24864
24938
|
console.debug(
|
|
24865
24939
|
`[route] connecting url param "${paramName}" to signal`,
|
|
@@ -24902,6 +24976,7 @@ const route = (
|
|
|
24902
24976
|
{
|
|
24903
24977
|
callReason: `${paramName} signal change on ${route}`,
|
|
24904
24978
|
isSignalChange: true,
|
|
24979
|
+
history: historyOfWrite(),
|
|
24905
24980
|
},
|
|
24906
24981
|
);
|
|
24907
24982
|
return;
|
|
@@ -24919,6 +24994,7 @@ const route = (
|
|
|
24919
24994
|
{
|
|
24920
24995
|
callReason: `${paramName} signal reset to default on ${route}`,
|
|
24921
24996
|
isSignalChange: true,
|
|
24997
|
+
history: historyOfWrite(),
|
|
24922
24998
|
},
|
|
24923
24999
|
);
|
|
24924
25000
|
return;
|
|
@@ -24938,6 +25014,7 @@ const route = (
|
|
|
24938
25014
|
{
|
|
24939
25015
|
callReason: `${paramName} signal change on ${route}`,
|
|
24940
25016
|
isSignalChange: true,
|
|
25017
|
+
history: historyOfWrite(),
|
|
24941
25018
|
},
|
|
24942
25019
|
);
|
|
24943
25020
|
});
|
|
@@ -30465,7 +30542,16 @@ const anyMatchingRouteSignal = (routes) => {
|
|
|
30465
30542
|
* @param {Element} element The element asking — the command's source, and the
|
|
30466
30543
|
* anchor a popup opens on unless `anchor` says otherwise.
|
|
30467
30544
|
* @param {string} command
|
|
30468
|
-
* @param {Event} event What
|
|
30545
|
+
* @param {Event} event What caused this. Mandatory: it is what makes a command
|
|
30546
|
+
* traceable back to its origin — the debug panel groups everything a gesture
|
|
30547
|
+
* set off under it, and the gates below read it to know whether the default
|
|
30548
|
+
* was already prevented and which mouse button was pressed. When nothing was
|
|
30549
|
+
* handed over — a timer firing, an action settling, a signal changing — build
|
|
30550
|
+
* a `CustomEvent` that names what happened and chain it to whatever preceded
|
|
30551
|
+
* it, rather than leaving the origin unsaid:
|
|
30552
|
+
* const expiredEvent = new CustomEvent("session_expired");
|
|
30553
|
+
* chainEvent(expiredEvent, causeEvent); // when something did precede it
|
|
30554
|
+
* triggerNaviCommand(dialogEl, "--navi-open", expiredEvent);
|
|
30469
30555
|
* @param {object} [options]
|
|
30470
30556
|
* @param {boolean} [options.optional] No suitable target is not a warning.
|
|
30471
30557
|
* @param {any} [options.value] What the command is about, carried to whoever
|
|
@@ -30482,6 +30568,11 @@ const triggerNaviCommand = (
|
|
|
30482
30568
|
event,
|
|
30483
30569
|
{ optional, value, anchor } = {},
|
|
30484
30570
|
) => {
|
|
30571
|
+
if (!event) {
|
|
30572
|
+
throw new Error(
|
|
30573
|
+
`"${command}" triggered without an event: it is mandatory, a command must say what caused it. Pass the gesture, or a CustomEvent naming the cause when no gesture did — see triggerNaviCommand's jsdoc.`,
|
|
30574
|
+
);
|
|
30575
|
+
}
|
|
30485
30576
|
const naviCommand =
|
|
30486
30577
|
NAVI_COMMANDS[command] || NAVI_COMMANDS[commandName(command)];
|
|
30487
30578
|
if (!naviCommand) {
|
|
@@ -33597,6 +33688,12 @@ const GROUP_DEFAULTS = {
|
|
|
33597
33688
|
*
|
|
33598
33689
|
* **Filtering**: `childControlFilter` can exclude certain child types from aggregation
|
|
33599
33690
|
* (e.g. ignoring buttons inside a selectable list).
|
|
33691
|
+
*
|
|
33692
|
+
* **Aggregating**: `aggregateChildStates(children, fallbackState, stateNow)` — what the
|
|
33693
|
+
* children add up to. `fallbackState` is the empty of the declared `stateType`;
|
|
33694
|
+
* `stateNow` is what the group is worth as it is asked, which an aggregate reads to
|
|
33695
|
+
* answer for children that are not there (a selectable list whose selected row is not
|
|
33696
|
+
* drawn).
|
|
33600
33697
|
*/
|
|
33601
33698
|
const useUIGroupStateController = (
|
|
33602
33699
|
props,
|
|
@@ -33753,16 +33850,22 @@ const useUIGroupStateController = (
|
|
|
33753
33850
|
// taking that for an answer is how a value handed to it evaporates on
|
|
33754
33851
|
// the way in, and how that emptiness then travels back up to whoever
|
|
33755
33852
|
// handed it (a picker showing its row as unanswered).
|
|
33756
|
-
|
|
33853
|
+
//
|
|
33854
|
+
// `stateNow` is what the group is worth as it is asked: what it keeps
|
|
33855
|
+
// when there is nobody to ask, and what an aggregate reads to answer for
|
|
33856
|
+
// what its children do not say — a selection whose row is not drawn (see
|
|
33857
|
+
// ListSelectable) lives there and nowhere else.
|
|
33858
|
+
const aggregateGroupUIState = (stateNow) => {
|
|
33757
33859
|
const someChildCanAnswer = childUIStateControllerArray.some(
|
|
33758
33860
|
shouldPropagateStateToChild,
|
|
33759
33861
|
);
|
|
33760
33862
|
if (!someChildCanAnswer) {
|
|
33761
|
-
return
|
|
33863
|
+
return stateNow;
|
|
33762
33864
|
}
|
|
33763
33865
|
const aggChildState = resolvedAggregateChildStates(
|
|
33764
33866
|
childUIStateControllerArray,
|
|
33765
33867
|
fallbackState,
|
|
33868
|
+
stateNow,
|
|
33766
33869
|
);
|
|
33767
33870
|
if (aggChildState !== undefined) {
|
|
33768
33871
|
return aggChildState;
|
|
@@ -34286,8 +34389,6 @@ const useUIGroupStateController = (
|
|
|
34286
34389
|
// ── update: runs every render after the first ─────────────────────────
|
|
34287
34390
|
(s) => {
|
|
34288
34391
|
const { controller } = s;
|
|
34289
|
-
const prevValue = controller.value;
|
|
34290
|
-
const prevHasValueProp = controller.hasValueProp;
|
|
34291
34392
|
const prevDefaultValue = controller.defaultValue;
|
|
34292
34393
|
controller.props = props;
|
|
34293
34394
|
controller.ref = ref;
|
|
@@ -34305,10 +34406,14 @@ const useUIGroupStateController = (
|
|
|
34305
34406
|
controller.placeChildrenUIState(groupUIState, propagateDownEvent);
|
|
34306
34407
|
controller.syncInternalState(groupUIState);
|
|
34307
34408
|
};
|
|
34308
|
-
|
|
34309
|
-
|
|
34310
|
-
|
|
34311
|
-
|
|
34409
|
+
// A controlled group goes on showing the value it is given. A child
|
|
34410
|
+
// answering for itself moves what the group is worth — that is what
|
|
34411
|
+
// `uiAction` reports — but the value stays the owner's, so the test is
|
|
34412
|
+
// against what the children are showing, not against the value handed
|
|
34413
|
+
// down last time. A popup reopened on another subject hands down the very
|
|
34414
|
+
// same empty value it did before, and the selection left inside it from
|
|
34415
|
+
// the previous opening is what has to go.
|
|
34416
|
+
if (hasValueProp && !compareTwoJsValues(value, controller.uiState)) {
|
|
34312
34417
|
placeChildrenFrom(value);
|
|
34313
34418
|
}
|
|
34314
34419
|
if (
|
|
@@ -38987,8 +39092,17 @@ const css$X = /* css */`
|
|
|
38987
39092
|
display: block;
|
|
38988
39093
|
min-width: 0;
|
|
38989
39094
|
text-overflow: ellipsis;
|
|
38990
|
-
|
|
39095
|
+
/* Only the inline axis has something to truncate, so the clip must not
|
|
39096
|
+
constrain the block one: overflow hidden would make the element a
|
|
39097
|
+
scroll container, whose flex automatic minimum size is 0, and inside a
|
|
39098
|
+
column flex container shorter than 1lh the text then shrinks under its
|
|
39099
|
+
own line box and gets cut through the glyphs. The margin leaves room
|
|
39100
|
+
for ink drawn outside the advance width. */
|
|
39101
|
+
overflow: clip;
|
|
38991
39102
|
overflow-wrap: normal;
|
|
39103
|
+
/* The padding-box keyword is redundant (it is the default box) but
|
|
39104
|
+
Chromium drops the declaration when a bare calc() follows. */
|
|
39105
|
+
overflow-clip-margin: padding-box calc((1lh - 1em) / 2);
|
|
38992
39106
|
}
|
|
38993
39107
|
|
|
38994
39108
|
&[data-skeleton] {
|
|
@@ -51102,6 +51216,10 @@ const causeOfEvent = event => {
|
|
|
51102
51216
|
}
|
|
51103
51217
|
return "command";
|
|
51104
51218
|
};
|
|
51219
|
+
|
|
51220
|
+
// The causes that are not places one went, whatever the state they are written
|
|
51221
|
+
// into says (see writeAreaAsked).
|
|
51222
|
+
const CAUSES_WRITTEN_BY_REPLACEMENT = ["drag", "refusal", "state"];
|
|
51105
51223
|
const readArea = slideElement => slideElement.getAttribute("data-slide-area") || slideElement.id || "";
|
|
51106
51224
|
|
|
51107
51225
|
/**
|
|
@@ -51138,33 +51256,65 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
|
|
|
51138
51256
|
* repeated spans), except that a row needs no trailing hole: what is not
|
|
51139
51257
|
* written is simply not there.
|
|
51140
51258
|
* @param {string} [props.current] - area (or id) of the slide being shown; omit
|
|
51141
|
-
* to keep it here and drive it by command.
|
|
51142
|
-
*
|
|
51143
|
-
*
|
|
51144
|
-
*
|
|
51145
|
-
*
|
|
51146
|
-
*
|
|
51147
|
-
*
|
|
51259
|
+
* to keep it here and drive it by command. An area named from outside is a
|
|
51260
|
+
* REQUEST rather than a placement — see `signal` for what that means.
|
|
51261
|
+
* @param {import("@preact/signals").Signal<string>} [props.signal] - where the
|
|
51262
|
+
* area lives, said the way every navi control says it: the container shows
|
|
51263
|
+
* the area the signal holds, and writes into it the area it travels to. One
|
|
51264
|
+
* binding instead of `current` + `onCurrentChange`, and the state stays where
|
|
51265
|
+
* the app put it — which is what lets something else read where the slides
|
|
51266
|
+
* are (a field carrying the current tab into a form, see
|
|
51148
51267
|
* docs/control_object.md#a-settings-sheet) or move them by writing it.
|
|
51149
51268
|
* Excludes `current`; `onCurrentChange` still fires, for the `cause` and for
|
|
51150
51269
|
* the right to refuse.
|
|
51151
|
-
*
|
|
51152
|
-
*
|
|
51270
|
+
* An area written from outside is WALKED to, not jumped to: every slide
|
|
51271
|
+
* between here and there is asked to let go the way a key going that way
|
|
51272
|
+
* would ask it, and the first one that holds (`preventNav`, a `required` step
|
|
51273
|
+
* still unanswered) is where one stops — after which the signal is written
|
|
51274
|
+
* with the area actually shown, so it says where one IS and never where one
|
|
51275
|
+
* asked to be.
|
|
51276
|
+
* PUTTING THE AREA IN THE URL is that binding and nothing more: hand it a
|
|
51277
|
+
* `stateSignal` a route declares as a search param, and `?step=<area>` is
|
|
51278
|
+
* written on every travel, read on a load, a bookmark, a link, a traversal —
|
|
51279
|
+
* through the walk above, so an address cannot open a slide the walk may not
|
|
51280
|
+
* reach.
|
|
51281
|
+
* ```js
|
|
51282
|
+
* const stepSignal = stateSignal("when", { id: "step", weak: true });
|
|
51283
|
+
* route("/alerts/:id/edit", { searchParams: { step: stepSignal } });
|
|
51284
|
+
* ```
|
|
51285
|
+
* ```jsx
|
|
51286
|
+
* <SlideContainer signal={stepSignal}>
|
|
51287
|
+
* ```
|
|
51288
|
+
* Where it opens is the state's own default (`stateSignal`'s first argument —
|
|
51289
|
+
* a signal there, for a start that depends on where one is), and the param
|
|
51290
|
+
* stays out of the address while the area IS that default. `weak` keeps the
|
|
51291
|
+
* step from being inherited by links built to that route. Written by
|
|
51292
|
+
* replacement unless the state says `history: "push"`, and even then a slide
|
|
51293
|
+
* reached by DRAGGING replaces — see docs/navigation.md.
|
|
51294
|
+
* @param {string} [props.defaultCurrent] - which slide to open on, for a
|
|
51295
|
+
* container that owns its position. Mount-only, like every other `default*`:
|
|
51153
51296
|
* it says where one starts, not where one is — say `current` for that.
|
|
51154
51297
|
* Without it the first slide is the one shown, the way a stack of pages opens
|
|
51155
51298
|
* on its first page.
|
|
51156
|
-
*
|
|
51299
|
+
* A container bound to a `signal` does not own its position, and where it
|
|
51300
|
+
* opens is that state's own default (`stateSignal`'s first argument) rather
|
|
51301
|
+
* than this: one place says where the area starts, and it is the place a
|
|
51302
|
+
* reset goes back to. This still answers for a signal holding nothing at
|
|
51303
|
+
* all.
|
|
51304
|
+
* @param {(area: string, detail: {cause: "drag"|"keyboard"|"command"|"code"|"state", event: Event}) => void|false|Promise<void|false>} [props.onCurrentChange]
|
|
51157
51305
|
* - the slide being shown has changed. `cause` says what asked for it, which
|
|
51158
|
-
* is what tells a place browsed past from a place aimed at
|
|
51159
|
-
*
|
|
51160
|
-
*
|
|
51161
|
-
*
|
|
51306
|
+
* is what tells a place browsed past from a place aimed at — a slide dragged
|
|
51307
|
+
* to rather than pressed for. `cause: "state"` is the bound state asking (a
|
|
51308
|
+
* write from the application, a load on a link, a traversal) rather than
|
|
51309
|
+
* anything done inside the box, and its `event` is null.
|
|
51162
51310
|
* Answer `false` to REFUSE the change and the slide goes back where it came
|
|
51163
51311
|
* from — a guard that says no, a session that is gone. A promise refuses it
|
|
51164
51312
|
* late, once whatever it had to ask has answered; the travel plays meanwhile
|
|
51165
51313
|
* and is undone if the answer is no.
|
|
51166
|
-
*
|
|
51167
|
-
*
|
|
51314
|
+
* It is not where the area is REMEMBERED: `signal` does that in both
|
|
51315
|
+
* directions and is written by every way of travelling, while a callback
|
|
51316
|
+
* assigning it catches only the ones it was written for — see
|
|
51317
|
+
* docs/state_binding.md. What is left here is the `cause` and the refusal.
|
|
51168
51318
|
* @param {"now"|"rest"} [props.commit="now"] - when the change is told.
|
|
51169
51319
|
* "rest" waits for the travel to be over, and lets the container hold the
|
|
51170
51320
|
* slide it is going to meanwhile: the picture moves with the finger and the
|
|
@@ -51182,33 +51332,6 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
|
|
|
51182
51332
|
* one that just travelled there. Called once the travel is over, and in the
|
|
51183
51333
|
* same render as the return to rest — anything later shows the old content
|
|
51184
51334
|
* for a frame.
|
|
51185
|
-
* @param {string|{name: string, history?: "replace"|"push"}} [props.urlParam]
|
|
51186
|
-
* - the search param this container owns, and where it stands is written
|
|
51187
|
-
* into: `urlParam="step"` puts `?step=<area>` in the address on every travel,
|
|
51188
|
-
* and opens on the area it names on a load, a bookmark, a traversal, a link
|
|
51189
|
-
* from anywhere else in the application.
|
|
51190
|
-
* It is the answer to "the step should be readable and should survive a
|
|
51191
|
-
* reload" WITHOUT one route per slide: no route to declare, no route guard to
|
|
51192
|
-
* re-express the walk's own rules in, and no page transition — what travels
|
|
51193
|
-
* is the box, and the address is a label on where the box stands. Say it on a
|
|
51194
|
-
* container whose slides are places one may be sent to; a walk nobody links
|
|
51195
|
-
* into keeps its position to itself.
|
|
51196
|
-
* Written by REPLACEMENT: four steps that each stacked an entry would turn
|
|
51197
|
-
* one back-press into four, and the back arrow of a page means "leave this
|
|
51198
|
-
* page", not "one question back". `history: "push"` says the opposite for the
|
|
51199
|
-
* containers that mean it — slides that ARE places one came from, a gallery
|
|
51200
|
-
* one browses — and even there a slide reached by dragging replaces, so
|
|
51201
|
-
* swiping back and forth does not bury the way out.
|
|
51202
|
-
* The address is READ through the walk, not jumped to: it comes from outside
|
|
51203
|
-
* the box, so every slide between here and there is asked to let go the way a
|
|
51204
|
-
* key going that way would ask it, and the first one that holds
|
|
51205
|
-
* (`preventNav`, or a `required` step still unanswered) is where one stops —
|
|
51206
|
-
* `?step=done` cannot open a confirmation screen for something nobody sent.
|
|
51207
|
-
* Whatever comes of it, the address is then rewritten with the area actually
|
|
51208
|
-
* shown: it says where one IS, never where one asked to be. A step the app
|
|
51209
|
-
* knows is already answered on a reload is told so by its own `required`
|
|
51210
|
-
* (`required={!alreadyFilled}`) — the container remembers nothing across a
|
|
51211
|
-
* load, and cannot.
|
|
51212
51335
|
* Each way of travelling can be shut off, or narrowed to one axis: `true`
|
|
51213
51336
|
* (every axis the map has), `false`, `"x"`, `"y"`, `"xy"`.
|
|
51214
51337
|
*
|
|
@@ -51261,7 +51384,6 @@ const SlideContainer = ({
|
|
|
51261
51384
|
commit = "now",
|
|
51262
51385
|
loop,
|
|
51263
51386
|
onLoop,
|
|
51264
|
-
urlParam,
|
|
51265
51387
|
travelByKeyboard = true,
|
|
51266
51388
|
travelByDrag = true,
|
|
51267
51389
|
travelByScroll = "x",
|
|
@@ -51278,7 +51400,11 @@ const SlideContainer = ({
|
|
|
51278
51400
|
const containerRef = useRef();
|
|
51279
51401
|
// The AREA of the slide being shown, not its rank: a rank would be wrong the
|
|
51280
51402
|
// moment a slide appears before it, and there is nothing to renumber here.
|
|
51281
|
-
|
|
51403
|
+
// This is where the container's own answer lives, and it is the only one the
|
|
51404
|
+
// picture is drawn from — an area coming from outside is a REQUEST that has
|
|
51405
|
+
// to be walked to first (see the effect reading currentFromCaller). At mount
|
|
51406
|
+
// there is no road to walk yet, so what the caller holds is taken as it is.
|
|
51407
|
+
const [currentAreaState, setCurrentAreaState] = useState(() => (currentSignal ? currentSignal.peek() : currentProp) ?? defaultCurrent);
|
|
51282
51408
|
// The slide this container is travelling to while its controller has not been
|
|
51283
51409
|
// told yet (commit="rest"): for the length of that travel the container is
|
|
51284
51410
|
// ahead of whoever holds `current`, and this is where it keeps its own answer
|
|
@@ -51356,51 +51482,42 @@ const SlideContainer = ({
|
|
|
51356
51482
|
// nothing measured per frame.
|
|
51357
51483
|
const followerElementsRef = useRef([]);
|
|
51358
51484
|
const currentFromCaller = currentSignal ? currentSignal.value : currentProp;
|
|
51359
|
-
const current = rollingArea ?? provisionalArea ??
|
|
51360
|
-
// The
|
|
51361
|
-
//
|
|
51362
|
-
|
|
51363
|
-
|
|
51364
|
-
// What the address says right now. Read from the signal rather than from
|
|
51365
|
-
// window.location so that a traversal — the browser's own back and forward —
|
|
51366
|
-
// is a render like any other: the address moved, and the slides follow it.
|
|
51367
|
-
// Read only when there is a param to read, so a container that owns none is
|
|
51368
|
-
// not re-rendered by every navigation in the application.
|
|
51369
|
-
const areaInUrl = urlParamName ? new URL(documentUrlSignal.value).searchParams.get(urlParamName) : null;
|
|
51370
|
-
// The last thing this container knows the address said, whether it wrote it
|
|
51371
|
-
// or read it. What makes a two-way binding stop turning: the address is only
|
|
51372
|
-
// an INSTRUCTION when it changed by itself (a load, a traversal, a link), and
|
|
51485
|
+
const current = rollingArea ?? provisionalArea ?? currentAreaState;
|
|
51486
|
+
// The last area this container knows the caller holds, whether it wrote it
|
|
51487
|
+
// there or read it from there. What makes a two-way binding stop turning: an
|
|
51488
|
+
// area held outside is an INSTRUCTION only when it changed by itself (a load,
|
|
51489
|
+
// a traversal, a link, something in the application writing the signal), and
|
|
51373
51490
|
// what this container put there is not news. Without it, a container ahead of
|
|
51374
|
-
// its own
|
|
51375
|
-
//
|
|
51376
|
-
const
|
|
51377
|
-
// Where the container stands, said
|
|
51378
|
-
//
|
|
51379
|
-
//
|
|
51380
|
-
//
|
|
51381
|
-
//
|
|
51382
|
-
//
|
|
51383
|
-
|
|
51384
|
-
|
|
51491
|
+
// its own state (commit="rest", where the picture arrives before the change
|
|
51492
|
+
// is told) would read its own lateness as an order to go back.
|
|
51493
|
+
const areaAskedSeenRef = useRef(currentSignal ? currentSignal.peek() : currentProp);
|
|
51494
|
+
// Where the container stands, said where the caller holds it.
|
|
51495
|
+
//
|
|
51496
|
+
// Whether a slide is a place one came from is the STATE's own business (see
|
|
51497
|
+
// stateSignal's `history`), and this container has no opinion on it. What it
|
|
51498
|
+
// does know is which of its own writes are not places at all, and those are
|
|
51499
|
+
// written by REPLACEMENT whatever the state declares:
|
|
51500
|
+
// - a drag, because browsing back and forth with a thumb is not a trail one
|
|
51501
|
+
// wants to walk home along;
|
|
51502
|
+
// - a correction — a refused travel put back, a walk that stopped before
|
|
51503
|
+
// where it was asked to go — because it is the entry that asked for the
|
|
51504
|
+
// wrong thing being amended, not a place one was;
|
|
51505
|
+
// - the state itself asking (`cause: "state"`), because reading a value back
|
|
51506
|
+
// and answering it is not somewhere one went.
|
|
51507
|
+
const writeAreaAsked = (area, {
|
|
51508
|
+
cause
|
|
51385
51509
|
} = {}) => {
|
|
51386
|
-
|
|
51510
|
+
areaAskedSeenRef.current = area;
|
|
51511
|
+
if (!currentSignal) {
|
|
51387
51512
|
return;
|
|
51388
51513
|
}
|
|
51389
|
-
|
|
51390
|
-
|
|
51391
|
-
|
|
51514
|
+
if (CAUSES_WRITTEN_BY_REPLACEMENT.includes(cause) && currentSignal.set) {
|
|
51515
|
+
currentSignal.set(area, {
|
|
51516
|
+
history: "replace"
|
|
51517
|
+
});
|
|
51392
51518
|
return;
|
|
51393
51519
|
}
|
|
51394
|
-
|
|
51395
|
-
navTo(urlObject.href, {
|
|
51396
|
-
replace: replace || urlParamHistory !== "push" || cause === "drag" ||
|
|
51397
|
-
// The address already says it: reading it back is not a place one went.
|
|
51398
|
-
cause === "url",
|
|
51399
|
-
// What travels is the box, not the page: the address is a label on where
|
|
51400
|
-
// the box stands, and a route transition would move the whole document
|
|
51401
|
-
// for a slide that has already travelled by itself.
|
|
51402
|
-
routeTransition: "none"
|
|
51403
|
-
});
|
|
51520
|
+
currentSignal.value = area;
|
|
51404
51521
|
};
|
|
51405
51522
|
const vertical = layout === "column";
|
|
51406
51523
|
// What the map has, and what each way of asking is allowed to use of it.
|
|
@@ -52031,7 +52148,7 @@ const SlideContainer = ({
|
|
|
52031
52148
|
}
|
|
52032
52149
|
const leftArea = readArea(currentElement);
|
|
52033
52150
|
setCurrentAreaState(area);
|
|
52034
|
-
if (!onCurrentChange && !currentSignal
|
|
52151
|
+
if (!onCurrentChange && !currentSignal) {
|
|
52035
52152
|
return true;
|
|
52036
52153
|
}
|
|
52037
52154
|
// What asked for this, read off the interaction rather than carried down
|
|
@@ -52062,21 +52179,16 @@ const SlideContainer = ({
|
|
|
52062
52179
|
return true;
|
|
52063
52180
|
};
|
|
52064
52181
|
|
|
52065
|
-
// The caller learns where the container went: the bound
|
|
52066
|
-
//
|
|
52067
|
-
//
|
|
52068
|
-
//
|
|
52069
|
-
// the
|
|
52070
|
-
//
|
|
52182
|
+
// The caller learns where the container went: the bound state is written
|
|
52183
|
+
// first, then `onCurrentChange` is called, so a caller reading the state from
|
|
52184
|
+
// inside its own handler reads where it now is. Written here rather than by
|
|
52185
|
+
// the caller so that it is told about the travels that HAPPENED and about no
|
|
52186
|
+
// others: the ones a lock refused never reach this point, and one refused
|
|
52187
|
+
// late is written back below (see goBackToRefusedArea).
|
|
52071
52188
|
const tellCurrentChange = (area, detail, leftArea) => {
|
|
52072
|
-
|
|
52073
|
-
|
|
52074
|
-
}
|
|
52075
|
-
// The address is told here rather than by the caller so that it is told
|
|
52076
|
-
// about the travels that HAPPENED and about no others: the ones a lock
|
|
52077
|
-
// refused never reach this point, and one refused late is written back
|
|
52078
|
-
// below (see goBackToRefusedArea).
|
|
52079
|
-
writeUrlParam(area, detail.cause);
|
|
52189
|
+
writeAreaAsked(area, {
|
|
52190
|
+
cause: detail.cause
|
|
52191
|
+
});
|
|
52080
52192
|
if (!onCurrentChange) {
|
|
52081
52193
|
return;
|
|
52082
52194
|
}
|
|
@@ -52104,47 +52216,44 @@ const SlideContainer = ({
|
|
|
52104
52216
|
const goBackToRefusedArea = leftArea => {
|
|
52105
52217
|
setProvisionalArea(null);
|
|
52106
52218
|
setCurrentAreaState(leftArea);
|
|
52107
|
-
|
|
52108
|
-
|
|
52109
|
-
|
|
52110
|
-
|
|
52111
|
-
|
|
52112
|
-
// being corrected, and a refusal is not a place one was.
|
|
52113
|
-
writeUrlParam(leftArea, "code", {
|
|
52114
|
-
replace: true
|
|
52219
|
+
// Written over rather than stacked on, whatever the state says otherwise:
|
|
52220
|
+
// the entry a refused travel wrote is the one being corrected, and a
|
|
52221
|
+
// refusal is not a place one was.
|
|
52222
|
+
writeAreaAsked(leftArea, {
|
|
52223
|
+
cause: "refusal"
|
|
52115
52224
|
});
|
|
52116
52225
|
};
|
|
52117
52226
|
|
|
52118
|
-
//
|
|
52119
|
-
//
|
|
52120
|
-
//
|
|
52121
|
-
//
|
|
52122
|
-
//
|
|
52123
|
-
//
|
|
52227
|
+
// Something outside the box asks for a slide: the application writing the
|
|
52228
|
+
// bound signal, and — when that signal is a route's own (see
|
|
52229
|
+
// docs/navigation.md) — a load on a link, a traversal, an address typed by
|
|
52230
|
+
// hand. All the same event here, and the answer to all of them is the same:
|
|
52231
|
+
// it is a REQUEST, walked to rather than jumped to.
|
|
52232
|
+
//
|
|
52233
|
+
// Walked, because the request comes from outside and the road may be closed:
|
|
52234
|
+
// every slide between here and there is asked to let go the way a key going
|
|
52235
|
+
// that way would ask it, and the first one that holds is where one stops.
|
|
52236
|
+
// `?step=done` cannot open a confirmation screen for something nobody sent.
|
|
52237
|
+
// Whatever comes of it, the state is then written with the area actually
|
|
52238
|
+
// shown: it says where one IS, never where one asked to be.
|
|
52124
52239
|
useLayoutEffect(() => {
|
|
52125
|
-
if (
|
|
52240
|
+
if (currentFromCaller === undefined) {
|
|
52126
52241
|
return;
|
|
52127
52242
|
}
|
|
52128
|
-
if (
|
|
52243
|
+
if (currentFromCaller === areaAskedSeenRef.current) {
|
|
52129
52244
|
// Not news: either nothing moved, or this container is reading back what
|
|
52130
52245
|
// it wrote itself.
|
|
52131
52246
|
return;
|
|
52132
52247
|
}
|
|
52133
|
-
|
|
52134
|
-
if (areaInUrl === null) {
|
|
52135
|
-
// An address that says nothing is not an address saying "the first
|
|
52136
|
-
// slide": a container opened without the param opens where it would have
|
|
52137
|
-
// opened anyway, and the param appears the first time one travels.
|
|
52138
|
-
return;
|
|
52139
|
-
}
|
|
52248
|
+
areaAskedSeenRef.current = currentFromCaller;
|
|
52140
52249
|
// Where the box IS, read off the DOM: `current` is undefined until someone
|
|
52141
52250
|
// names a slide, and the container standing on its first one is a fact only
|
|
52142
52251
|
// the map knows (see the layout effect that paints it).
|
|
52143
|
-
const areaOnScreen = containerRef.current?.getAttribute(
|
|
52144
|
-
if (!areaOnScreen ||
|
|
52252
|
+
const areaOnScreen = containerRef.current?.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
|
|
52253
|
+
if (!areaOnScreen || currentFromCaller === areaOnScreen) {
|
|
52145
52254
|
return;
|
|
52146
52255
|
}
|
|
52147
|
-
const reached = reachableTowards(areaOnScreen,
|
|
52256
|
+
const reached = reachableTowards(areaOnScreen, currentFromCaller);
|
|
52148
52257
|
if (reached && reached !== areaOnScreen) {
|
|
52149
52258
|
// Told rather than asked: the walk above has already put every lock on
|
|
52150
52259
|
// the way the question goToArea would have put the first one, and this
|
|
@@ -52153,15 +52262,25 @@ const SlideContainer = ({
|
|
|
52153
52262
|
// any travel nobody pressed anything for.
|
|
52154
52263
|
setCurrentAreaState(reached);
|
|
52155
52264
|
tellCurrentChange(reached, {
|
|
52156
|
-
cause: "
|
|
52265
|
+
cause: "state"
|
|
52157
52266
|
}, areaOnScreen);
|
|
52158
52267
|
return;
|
|
52159
52268
|
}
|
|
52160
|
-
//
|
|
52161
|
-
//
|
|
52162
|
-
//
|
|
52163
|
-
|
|
52164
|
-
|
|
52269
|
+
// Nowhere this map knows, or somewhere the walk is not allowed to reach:
|
|
52270
|
+
// the state is put back on the slide one is actually looking at, rather
|
|
52271
|
+
// than left saying one is somewhere one is not. A caller holding `current`
|
|
52272
|
+
// themselves is told instead — it is their value to correct.
|
|
52273
|
+
if (currentSignal) {
|
|
52274
|
+
writeAreaAsked(areaOnScreen, {
|
|
52275
|
+
cause: "refusal"
|
|
52276
|
+
});
|
|
52277
|
+
return;
|
|
52278
|
+
}
|
|
52279
|
+
onCurrentChange?.(areaOnScreen, {
|
|
52280
|
+
cause: "state",
|
|
52281
|
+
event: null
|
|
52282
|
+
});
|
|
52283
|
+
}, [currentFromCaller]);
|
|
52165
52284
|
|
|
52166
52285
|
// The press kept during a roll, taken once the window rests and the travel is
|
|
52167
52286
|
// given back (noTravel off): by direction when there was one, so it is read
|
|
@@ -53259,6 +53378,10 @@ const SlideContainer = ({
|
|
|
53259
53378
|
* ahead to a screen that has nothing to show yet. Answering a step
|
|
53260
53379
|
* un-answers the ones after it, since what they were answered about has just
|
|
53261
53380
|
* changed.
|
|
53381
|
+
* A container remembers nothing across a load, and cannot: a step the app
|
|
53382
|
+
* knows is already answered when the page opens says so itself
|
|
53383
|
+
* (`required={!alreadyFilled}`), which is what lets an address naming a later
|
|
53384
|
+
* step be walked to.
|
|
53262
53385
|
* @param {boolean} [props.preventNav] - hold the user here, whichever way they
|
|
53263
53386
|
* try to leave.
|
|
53264
53387
|
* @param {boolean} [props.preventNavNext] - hold them from going right or down.
|
|
@@ -61346,23 +61469,22 @@ const ListSelectable = props => {
|
|
|
61346
61469
|
focusGroupDirection,
|
|
61347
61470
|
focusGroupWrap
|
|
61348
61471
|
} = props;
|
|
61349
|
-
//
|
|
61350
|
-
// draws the rows it needs and no more
|
|
61351
|
-
// of the window
|
|
61352
|
-
//
|
|
61353
|
-
//
|
|
61354
|
-
|
|
61355
|
-
|
|
61356
|
-
|
|
61357
|
-
|
|
61472
|
+
// `kept` is what the list holds as it is asked, which is not the same as what
|
|
61473
|
+
// its rows say: a list draws the rows it needs and no more, so the selected
|
|
61474
|
+
// one may be scrolled out of the window or filtered out of the view, and a
|
|
61475
|
+
// row that is not there cannot say it is not selected. Reading it off the
|
|
61476
|
+
// group rather than remembering it here is what makes a value put ON the list
|
|
61477
|
+
// (a `value` prop, a signal, a reopened popup) replace the whole selection —
|
|
61478
|
+
// a private memory of its own would go on holding the rows it could not see
|
|
61479
|
+
// being unselected.
|
|
61480
|
+
//
|
|
61358
61481
|
// `fallbackState` is the empty of the shape the list declared below
|
|
61359
61482
|
// (`stateType`): `[]` for a multiple list, nothing for a single one. Taking
|
|
61360
61483
|
// it is what lets an emptied list say "empty" — `undefined` is the word for
|
|
61361
61484
|
// "unset", and a bound stateSignal reads that as "nothing decided here, go
|
|
61362
61485
|
// back to the default" (see docs/control_value.md), which is how a list
|
|
61363
61486
|
// emptied down to its last row puts that row back on reload.
|
|
61364
|
-
const aggregateChildStates = (children, fallbackState) => {
|
|
61365
|
-
const kept = selectionRef.current;
|
|
61487
|
+
const aggregateChildStates = (children, fallbackState, kept) => {
|
|
61366
61488
|
if (multiple) {
|
|
61367
61489
|
const drawnValues = new Set(children.map(child => child.props.value));
|
|
61368
61490
|
const stillSelected = Array.isArray(kept) ? kept.filter(value => !drawnValues.has(value)) : [];
|
|
@@ -61371,13 +61493,10 @@ const ListSelectable = props => {
|
|
|
61371
61493
|
stillSelected.push(child.uiState);
|
|
61372
61494
|
}
|
|
61373
61495
|
}
|
|
61374
|
-
|
|
61375
|
-
selectionRef.current = values;
|
|
61376
|
-
return values;
|
|
61496
|
+
return stillSelected.length === 0 ? fallbackState : stillSelected;
|
|
61377
61497
|
}
|
|
61378
61498
|
for (const child of children) {
|
|
61379
61499
|
if (child.uiState !== undefined) {
|
|
61380
|
-
selectionRef.current = child.uiState;
|
|
61381
61500
|
return child.uiState;
|
|
61382
61501
|
}
|
|
61383
61502
|
}
|
|
@@ -61385,7 +61504,6 @@ const ListSelectable = props => {
|
|
|
61385
61504
|
// deselected; if it is not, the list keeps what it holds.
|
|
61386
61505
|
const keptIsDrawn = children.some(child => child.props.value === kept);
|
|
61387
61506
|
if (keptIsDrawn) {
|
|
61388
|
-
selectionRef.current = undefined;
|
|
61389
61507
|
return undefined;
|
|
61390
61508
|
}
|
|
61391
61509
|
return kept;
|