55import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
66export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
77import { 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";
99import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
1010import { isValidElement, h, Fragment, createContext, render, toChildArray, options, cloneElement } from "preact";
1111import { useErrorBoundary, useLayoutEffect, useContext, useCallback, useRef, useState, useEffect, useMemo, useId } from "preact/hooks";
@@ -30465,7 +30465,16 @@ const anyMatchingRouteSignal = (routes) => {
3046530465 * @param {Element} element The element asking — the command's source, and the
3046630466 * anchor a popup opens on unless `anchor` says otherwise.
3046730467 * @param {string} command
30468- * @param {Event} event What the user did.
30468+ * @param {Event} event What caused this. Mandatory: it is what makes a command
30469+ * traceable back to its origin — the debug panel groups everything a gesture
30470+ * set off under it, and the gates below read it to know whether the default
30471+ * was already prevented and which mouse button was pressed. When nothing was
30472+ * handed over — a timer firing, an action settling, a signal changing — build
30473+ * a `CustomEvent` that names what happened and chain it to whatever preceded
30474+ * it, rather than leaving the origin unsaid:
30475+ * const expiredEvent = new CustomEvent("session_expired");
30476+ * chainEvent(expiredEvent, causeEvent); // when something did precede it
30477+ * triggerNaviCommand(dialogEl, "--navi-open", expiredEvent);
3046930478 * @param {object} [options]
3047030479 * @param {boolean} [options.optional] No suitable target is not a warning.
3047130480 * @param {any} [options.value] What the command is about, carried to whoever
@@ -30482,6 +30491,11 @@ const triggerNaviCommand = (
3048230491 event,
3048330492 { optional, value, anchor } = {},
3048430493) => {
30494+ if (!event) {
30495+ throw new Error(
30496+ `"${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.`,
30497+ );
30498+ }
3048530499 const naviCommand =
3048630500 NAVI_COMMANDS[command] || NAVI_COMMANDS[commandName(command)];
3048730501 if (!naviCommand) {
@@ -33597,6 +33611,12 @@ const GROUP_DEFAULTS = {
3359733611 *
3359833612 * **Filtering**: `childControlFilter` can exclude certain child types from aggregation
3359933613 * (e.g. ignoring buttons inside a selectable list).
33614+ *
33615+ * **Aggregating**: `aggregateChildStates(children, fallbackState, stateNow)` — what the
33616+ * children add up to. `fallbackState` is the empty of the declared `stateType`;
33617+ * `stateNow` is what the group is worth as it is asked, which an aggregate reads to
33618+ * answer for children that are not there (a selectable list whose selected row is not
33619+ * drawn).
3360033620 */
3360133621const useUIGroupStateController = (
3360233622 props,
@@ -33753,16 +33773,22 @@ const useUIGroupStateController = (
3375333773 // taking that for an answer is how a value handed to it evaporates on
3375433774 // the way in, and how that emptiness then travels back up to whoever
3375533775 // handed it (a picker showing its row as unanswered).
33756- const aggregateGroupUIState = (whenNobodyCanAnswer) => {
33776+ //
33777+ // `stateNow` is what the group is worth as it is asked: what it keeps
33778+ // when there is nobody to ask, and what an aggregate reads to answer for
33779+ // what its children do not say — a selection whose row is not drawn (see
33780+ // ListSelectable) lives there and nowhere else.
33781+ const aggregateGroupUIState = (stateNow) => {
3375733782 const someChildCanAnswer = childUIStateControllerArray.some(
3375833783 shouldPropagateStateToChild,
3375933784 );
3376033785 if (!someChildCanAnswer) {
33761- return whenNobodyCanAnswer ;
33786+ return stateNow ;
3376233787 }
3376333788 const aggChildState = resolvedAggregateChildStates(
3376433789 childUIStateControllerArray,
3376533790 fallbackState,
33791+ stateNow,
3376633792 );
3376733793 if (aggChildState !== undefined) {
3376833794 return aggChildState;
@@ -34286,8 +34312,6 @@ const useUIGroupStateController = (
3428634312 // ── update: runs every render after the first ─────────────────────────
3428734313 (s) => {
3428834314 const { controller } = s;
34289- const prevValue = controller.value;
34290- const prevHasValueProp = controller.hasValueProp;
3429134315 const prevDefaultValue = controller.defaultValue;
3429234316 controller.props = props;
3429334317 controller.ref = ref;
@@ -34305,10 +34329,14 @@ const useUIGroupStateController = (
3430534329 controller.placeChildrenUIState(groupUIState, propagateDownEvent);
3430634330 controller.syncInternalState(groupUIState);
3430734331 };
34308- if (
34309- hasValueProp &&
34310- (!prevHasValueProp || !compareTwoJsValues(value, prevValue))
34311- ) {
34332+ // A controlled group goes on showing the value it is given. A child
34333+ // answering for itself moves what the group is worth — that is what
34334+ // `uiAction` reports — but the value stays the owner's, so the test is
34335+ // against what the children are showing, not against the value handed
34336+ // down last time. A popup reopened on another subject hands down the very
34337+ // same empty value it did before, and the selection left inside it from
34338+ // the previous opening is what has to go.
34339+ if (hasValueProp && !compareTwoJsValues(value, controller.uiState)) {
3431234340 placeChildrenFrom(value);
3431334341 }
3431434342 if (
@@ -61346,23 +61374,22 @@ const ListSelectable = props => {
6134661374 focusGroupDirection,
6134761375 focusGroupWrap
6134861376 } = props;
61349- // What the list holds, which is not the same as what its rows say. A list
61350- // draws the rows it needs and no more: the selected one may be scrolled out
61351- // of the window, or filtered out of the view. Aggregating over the rows that
61352- // happen to be mounted would then lose the selection — a row that is not
61353- // there cannot say it is not selected.
61354- const selectionRef = useRef(undefined);
61355- if (selectionRef.current === undefined) {
61356- selectionRef.current = Object.hasOwn(props, "value") ? props.value : props.defaultValue;
61357- }
61377+ // `kept` is what the list holds as it is asked , which is not the same as what
61378+ // its rows say: a list draws the rows it needs and no more, so the selected
61379+ // one may be scrolled out of the window or filtered out of the view, and a
61380+ // row that is not there cannot say it is not selected. Reading it off the
61381+ // group rather than remembering it here is what makes a value put ON the list
61382+ // (a `value` prop, a signal, a reopened popup) replace the whole selection —
61383+ // a private memory of its own would go on holding the rows it could not see
61384+ // being unselected.
61385+ //
6135861386 // `fallbackState` is the empty of the shape the list declared below
6135961387 // (`stateType`): `[]` for a multiple list, nothing for a single one. Taking
6136061388 // it is what lets an emptied list say "empty" — `undefined` is the word for
6136161389 // "unset", and a bound stateSignal reads that as "nothing decided here, go
6136261390 // back to the default" (see docs/control_value.md), which is how a list
6136361391 // emptied down to its last row puts that row back on reload.
61364- const aggregateChildStates = (children, fallbackState) => {
61365- const kept = selectionRef.current;
61392+ const aggregateChildStates = (children, fallbackState, kept) => {
6136661393 if (multiple) {
6136761394 const drawnValues = new Set(children.map(child => child.props.value));
6136861395 const stillSelected = Array.isArray(kept) ? kept.filter(value => !drawnValues.has(value)) : [];
@@ -61371,21 +61398,17 @@ const ListSelectable = props => {
6137161398 stillSelected.push(child.uiState);
6137261399 }
6137361400 }
61374- const values = stillSelected.length === 0 ? fallbackState : stillSelected;
61375- selectionRef.current = values;
61376- return values;
61401+ return stillSelected.length === 0 ? fallbackState : stillSelected;
6137761402 }
6137861403 for (const child of children) {
6137961404 if (child.uiState !== undefined) {
61380- selectionRef.current = child.uiState;
6138161405 return child.uiState;
6138261406 }
6138361407 }
6138461408 // No drawn row claims it. If the row that held it IS drawn, it was really
6138561409 // deselected; if it is not, the list keeps what it holds.
6138661410 const keptIsDrawn = children.some(child => child.props.value === kept);
6138761411 if (keptIsDrawn) {
61388- selectionRef.current = undefined;
6138961412 return undefined;
6139061413 }
6139161414 return kept;
0 commit comments