@jsenv/navi 0.29.141 → 0.29.142

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.
@@ -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";
@@ -30465,7 +30465,16 @@ const anyMatchingRouteSignal = (routes) => {
30465
30465
  * @param {Element} element The element asking — the command's source, and the
30466
30466
  * anchor a popup opens on unless `anchor` says otherwise.
30467
30467
  * @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);
30469
30478
  * @param {object} [options]
30470
30479
  * @param {boolean} [options.optional] No suitable target is not a warning.
30471
30480
  * @param {any} [options.value] What the command is about, carried to whoever
@@ -30482,6 +30491,11 @@ const triggerNaviCommand = (
30482
30491
  event,
30483
30492
  { optional, value, anchor } = {},
30484
30493
  ) => {
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
+ }
30485
30499
  const naviCommand =
30486
30500
  NAVI_COMMANDS[command] || NAVI_COMMANDS[commandName(command)];
30487
30501
  if (!naviCommand) {
@@ -33597,6 +33611,12 @@ const GROUP_DEFAULTS = {
33597
33611
  *
33598
33612
  * **Filtering**: `childControlFilter` can exclude certain child types from aggregation
33599
33613
  * (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).
33600
33620
  */
33601
33621
  const useUIGroupStateController = (
33602
33622
  props,
@@ -33753,16 +33773,22 @@ const useUIGroupStateController = (
33753
33773
  // taking that for an answer is how a value handed to it evaporates on
33754
33774
  // the way in, and how that emptiness then travels back up to whoever
33755
33775
  // 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) => {
33757
33782
  const someChildCanAnswer = childUIStateControllerArray.some(
33758
33783
  shouldPropagateStateToChild,
33759
33784
  );
33760
33785
  if (!someChildCanAnswer) {
33761
- return whenNobodyCanAnswer;
33786
+ return stateNow;
33762
33787
  }
33763
33788
  const aggChildState = resolvedAggregateChildStates(
33764
33789
  childUIStateControllerArray,
33765
33790
  fallbackState,
33791
+ stateNow,
33766
33792
  );
33767
33793
  if (aggChildState !== undefined) {
33768
33794
  return aggChildState;
@@ -34286,8 +34312,6 @@ const useUIGroupStateController = (
34286
34312
  // ── update: runs every render after the first ─────────────────────────
34287
34313
  (s) => {
34288
34314
  const { controller } = s;
34289
- const prevValue = controller.value;
34290
- const prevHasValueProp = controller.hasValueProp;
34291
34315
  const prevDefaultValue = controller.defaultValue;
34292
34316
  controller.props = props;
34293
34317
  controller.ref = ref;
@@ -34305,10 +34329,14 @@ const useUIGroupStateController = (
34305
34329
  controller.placeChildrenUIState(groupUIState, propagateDownEvent);
34306
34330
  controller.syncInternalState(groupUIState);
34307
34331
  };
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)) {
34312
34340
  placeChildrenFrom(value);
34313
34341
  }
34314
34342
  if (
@@ -61346,23 +61374,22 @@ const ListSelectable = props => {
61346
61374
  focusGroupDirection,
61347
61375
  focusGroupWrap
61348
61376
  } = 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
+ //
61358
61386
  // `fallbackState` is the empty of the shape the list declared below
61359
61387
  // (`stateType`): `[]` for a multiple list, nothing for a single one. Taking
61360
61388
  // it is what lets an emptied list say "empty" — `undefined` is the word for
61361
61389
  // "unset", and a bound stateSignal reads that as "nothing decided here, go
61362
61390
  // back to the default" (see docs/control_value.md), which is how a list
61363
61391
  // 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) => {
61366
61393
  if (multiple) {
61367
61394
  const drawnValues = new Set(children.map(child => child.props.value));
61368
61395
  const stillSelected = Array.isArray(kept) ? kept.filter(value => !drawnValues.has(value)) : [];
@@ -61371,13 +61398,10 @@ const ListSelectable = props => {
61371
61398
  stillSelected.push(child.uiState);
61372
61399
  }
61373
61400
  }
61374
- const values = stillSelected.length === 0 ? fallbackState : stillSelected;
61375
- selectionRef.current = values;
61376
- return values;
61401
+ return stillSelected.length === 0 ? fallbackState : stillSelected;
61377
61402
  }
61378
61403
  for (const child of children) {
61379
61404
  if (child.uiState !== undefined) {
61380
- selectionRef.current = child.uiState;
61381
61405
  return child.uiState;
61382
61406
  }
61383
61407
  }
@@ -61385,7 +61409,6 @@ const ListSelectable = props => {
61385
61409
  // deselected; if it is not, the list keeps what it holds.
61386
61410
  const keptIsDrawn = children.some(child => child.props.value === kept);
61387
61411
  if (keptIsDrawn) {
61388
- selectionRef.current = undefined;
61389
61412
  return undefined;
61390
61413
  }
61391
61414
  return kept;