@jsenv/navi 0.29.37 → 0.29.39

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.
@@ -3,7 +3,7 @@
3
3
  * using @jsenv/navi as intended.
4
4
  */
5
5
  import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, coarsePointerSignal } from "./jsenv_navi_side_effects.js";
6
- import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
6
+ import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
7
7
  export { contrastColor, findEvent, startDragTo } from "@jsenv/dom";
8
8
  import { signal, computed, effect, batch, useSignal } from "@preact/signals";
9
9
  import { createContext, isValidElement, h, Fragment, render, toChildArray, options, cloneElement } from "preact";
@@ -12550,10 +12550,11 @@ defineInteractionDetector({
12550
12550
  if (canReorder) {
12551
12551
  element.setAttribute(REORDERABLE_ATTRIBUTE, "");
12552
12552
  }
12553
- // What @jsenv/dom puts on a drag source: no iOS callout, and the touch left to
12554
- // the scroll until the press becomes a grab. Its value is the axis the
12555
- // SURROUNDINGS scroll on, which for a list is the axis the list runs on.
12556
- element.setAttribute("data-drag-source", axes === "x" ? "x" : "");
12553
+ // What @jsenv/dom puts on a drag source: no iOS callout, the touch left to
12554
+ // the scroll until the press becomes a grab, and the listener that lets the
12555
+ // grab take it back. Its argument is the axis the SURROUNDINGS scroll on,
12556
+ // which for a list is the axis the list runs on.
12557
+ const unmarkDragSource = markDragSource(element, axes);
12557
12558
 
12558
12559
  // What a release can mean, which is not all of what was declared: "grab" is a
12559
12560
  // moment, not an outcome, and the gesture must not read it as one.
@@ -12630,7 +12631,7 @@ defineInteractionDetector({
12630
12631
 
12631
12632
  return () => {
12632
12633
  element.removeAttribute(REORDERABLE_ATTRIBUTE);
12633
- element.removeAttribute("data-drag-source");
12634
+ unmarkDragSource();
12634
12635
  element.removeEventListener("pointerdown", onPointerDown);
12635
12636
  };
12636
12637
  },
@@ -23707,13 +23708,13 @@ const useUIGroupStateController = (
23707
23708
  );
23708
23709
  }
23709
23710
  const parentUIStateController = useContext(ParentUIStateControllerContext);
23710
- const hasValueProp = Object.hasOwn(props, "value");
23711
- const hasOwnDefaultValueProp = Object.hasOwn(props, "defaultValue");
23712
23711
  // A bound signal seeds the group the way `defaultValue` does — uncontrolled,
23713
23712
  // with the signal's current value as what it starts on. Write-back is handled
23714
23713
  // by applyState's own boundSignal; the read half is below: children are
23715
23714
  // placed from it when they register, and again whenever it moves.
23716
- const boundSignal = hasValueProp ? undefined : props.signal;
23715
+ const boundSignal = props.signal;
23716
+ const hasValueProp = !boundSignal && Object.hasOwn(props, "value");
23717
+ const hasOwnDefaultValueProp = Object.hasOwn(props, "defaultValue");
23717
23718
  const hasDefaultValueProp = hasOwnDefaultValueProp || Boolean(boundSignal);
23718
23719
  const { id, name, value, uiAction } = props;
23719
23720
  // A signal holding something wins over `defaultValue`: the default is a
@@ -24123,6 +24124,11 @@ const useUIGroupStateController = (
24123
24124
  parentUIStateController,
24124
24125
  uiAction,
24125
24126
  uiActionInternal,
24127
+ // `props` is what writeBoundSignal reads to find the bound `signal`.
24128
+ // Missing here, a group whose component never re-renders between mount
24129
+ // and the first choice wrote nothing back into its signal — and said
24130
+ // nothing about it: the list showed the choice, the signal stayed empty.
24131
+ props,
24126
24132
  };
24127
24133
  },
24128
24134
  // ── update: runs every render after the first ─────────────────────────
@@ -25321,22 +25327,25 @@ const createControlInfo = (props, {
25321
25327
  defaultStatePropName = "defaultChecked";
25322
25328
  value = props.value || "on";
25323
25329
  signalHoldsChecked = true;
25324
- if (Object.hasOwn(props, "checked")) {
25330
+ if (signal) {
25331
+ if (props.defaultChecked) {
25332
+ // resolveInputProps may seed defaultChecked from a bound signal's
25333
+ // default: the control stays uncontrolled and follows the signal
25334
+ // through stateFromSignal.
25335
+ hasStateProp = false;
25336
+ stateInitial = value;
25337
+ stateFromSignal = signal.value ? value : undefined;
25338
+ } else {
25339
+ // A bound signal with no resolved default: its live value seeds state.
25340
+ hasStateProp = true;
25341
+ stateInitial = signal.value ? value : undefined;
25342
+ }
25343
+ } else if (Object.hasOwn(props, "checked")) {
25325
25344
  hasStateProp = true;
25326
25345
  stateInitial = props.checked ? value : undefined;
25327
25346
  } else if (props.defaultChecked) {
25328
- // resolveInputProps may seed defaultChecked from a bound signal's
25329
- // default: the control stays uncontrolled and follows the signal
25330
- // through stateFromSignal.
25331
25347
  hasStateProp = false;
25332
25348
  stateInitial = value;
25333
- if (signal) {
25334
- stateFromSignal = signal.value ? value : undefined;
25335
- }
25336
- } else if (signal) {
25337
- // A bound signal with no resolved default: its live value seeds state.
25338
- hasStateProp = true;
25339
- stateInitial = signal.value ? value : undefined;
25340
25349
  } else {
25341
25350
  hasStateProp = false;
25342
25351
  stateInitial = undefined;
@@ -25344,30 +25353,33 @@ const createControlInfo = (props, {
25344
25353
  } else {
25345
25354
  statePropName = "value";
25346
25355
  defaultStatePropName = "defaultValue";
25347
- if (Object.hasOwn(props, "value")) {
25356
+ if (signal) {
25357
+ if (Object.hasOwn(props, "defaultValue")) {
25358
+ // resolveInputProps seeds defaultValue from a bound signal's default,
25359
+ // so an input+signal is uncontrolled-with-default; the signal only
25360
+ // receives write-backs (onUIAction).
25361
+ hasStateProp = false;
25362
+ // A signal holding something wins over the default: `defaultValue` is
25363
+ // a suggestion of what to start from (and what a reset goes back to),
25364
+ // not an answer — while the signal's value IS the answer, restored
25365
+ // from the url or set by whoever owns it. Taking the default here
25366
+ // would show a suggestion in place of the value on every reload.
25367
+ stateInitial = signal.value !== undefined ? signal.value : props.defaultValue;
25368
+ stateFromSignal = stateInitial;
25369
+ } else {
25370
+ // A plain bound signal with no default (e.g. Wheel): its live value
25371
+ // seeds and controls the state.
25372
+ hasStateProp = true;
25373
+ value = signal.value;
25374
+ stateInitial = value;
25375
+ }
25376
+ } else if (Object.hasOwn(props, "value")) {
25348
25377
  hasStateProp = true;
25349
25378
  value = props.value;
25350
25379
  stateInitial = value;
25351
25380
  } else if (Object.hasOwn(props, "defaultValue")) {
25352
- // resolveInputProps seeds defaultValue from a bound signal's default,
25353
- // so an input+signal is uncontrolled-with-default; the signal only
25354
- // receives write-backs (onUIAction).
25355
25381
  hasStateProp = false;
25356
- // A signal holding something wins over the default: `defaultValue` is a
25357
- // suggestion of what to start from (and what a reset goes back to), not
25358
- // an answer — while the signal's value IS the answer, restored from the
25359
- // url or set by whoever owns it. Taking the default here would show a
25360
- // suggestion in place of the value on every reload.
25361
- stateInitial = signal && signal.value !== undefined ? signal.value : props.defaultValue;
25362
- if (signal) {
25363
- stateFromSignal = stateInitial;
25364
- }
25365
- } else if (signal) {
25366
- // A plain bound signal with no default (e.g. Wheel): its live value
25367
- // seeds and controls the state.
25368
- hasStateProp = true;
25369
- value = signal.value;
25370
- stateInitial = value;
25382
+ stateInitial = props.defaultValue;
25371
25383
  } else {
25372
25384
  hasStateProp = false;
25373
25385
  stateInitial = undefined;
@@ -25387,20 +25399,23 @@ const createControlInfo = (props, {
25387
25399
  } else if (controlType === "picker" || controlType === "select") {
25388
25400
  statePropName = "value";
25389
25401
  defaultStatePropName = "defaultValue";
25390
- if (Object.hasOwn(props, "value")) {
25402
+ if (signal) {
25403
+ if (Object.hasOwn(props, "defaultValue")) {
25404
+ hasStateProp = false;
25405
+ // The signal's value is the answer, defaultValue only the suggestion to
25406
+ // start from.
25407
+ stateInitial = signal.value !== undefined ? signal.value : props.defaultValue;
25408
+ stateFromSignal = stateInitial;
25409
+ } else {
25410
+ hasStateProp = true;
25411
+ stateInitial = signal.value;
25412
+ }
25413
+ } else if (Object.hasOwn(props, "value")) {
25391
25414
  hasStateProp = true;
25392
25415
  stateInitial = props.value;
25393
25416
  } else if (Object.hasOwn(props, "defaultValue")) {
25394
25417
  hasStateProp = false;
25395
- // Same precedence as an input above: the signal's value is the answer,
25396
- // defaultValue only the suggestion to start from.
25397
- stateInitial = signal && signal.value !== undefined ? signal.value : props.defaultValue;
25398
- if (signal) {
25399
- stateFromSignal = stateInitial;
25400
- }
25401
- } else if (signal) {
25402
- hasStateProp = true;
25403
- stateInitial = signal.value;
25418
+ stateInitial = props.defaultValue;
25404
25419
  } else {
25405
25420
  hasStateProp = false;
25406
25421
  stateInitial = undefined;
@@ -26901,7 +26916,19 @@ const findFocusTarget = (containerEl) => {
26901
26916
  // Neither is dropped, both are simply tried later — step 3 below for the
26902
26917
  // first, and for the second the restore transferFocus does before ever
26903
26918
  // calling here.
26904
- const skip = (element) => isRestorableAutofocus(element);
26919
+ //
26920
+ // Skipped for good, unlike the two above: an element hidden from assistive
26921
+ // technology is not a place the focus can land at all. Something aria-hidden
26922
+ // and out of the tab order is a value holder standing behind what one
26923
+ // actually uses — a spin's headless picker behind its slides, say — and
26924
+ // landing there puts a ring on it, raises a phone's keyboard over the panel
26925
+ // that just opened, and has the browser complain about a focused aria-hidden
26926
+ // element. What one came to use is further down the same container.
26927
+ const isHiddenFromAssistiveTech = (element) =>
26928
+ Boolean(element.closest?.(`[aria-hidden="true"]`));
26929
+
26930
+ const skip = (element) =>
26931
+ isRestorableAutofocus(element) || isHiddenFromAssistiveTech(element);
26905
26932
 
26906
26933
  // Every mark, not just the first: a mark is only worth stopping at if it
26907
26934
  // leads somewhere focusable. One inside a screen waiting its turn (an inert
@@ -46865,7 +46892,10 @@ const resolveInputProps = (props) => {
46865
46892
  if (Object.hasOwn(props, "defaultChecked")) ; else {
46866
46893
  // If no explicit defaultChecked, derive it from the signal's default
46867
46894
  // value so that resetUIState restores to the original default.
46868
- const defaultVal = signal.options.getDefaultValue(false);
46895
+ // Only a stateSignal carries a default of its own; a plain signal has
46896
+ // no `options` at all, and asking it for one used to throw on mount —
46897
+ // the same optional read every other branch here already does.
46898
+ const defaultVal = signalOptions?.getDefaultValue(false);
46869
46899
  if (defaultVal === undefined) ; else if (props.type === "radio") {
46870
46900
  if (defaultVal === true) {
46871
46901
  props.defaultChecked = true;
@@ -58915,10 +58945,15 @@ const PickerFirstResolver = props => {
58915
58945
  * picker. "cancel" puts back the value the picker had at open, and a dialog
58916
58946
  * picker also goes back in history — so anything written to the url while it
58917
58947
  * was open (a route `stateSignal`, a search param) goes back with it. "close"
58918
- * makes Escape say what clicking outside says: keep what was chosen, close.
58948
+ * makes Escape say what clicking outside says: keep what was chosen, close
58949
+ * a last resort, see docs/popup_open.md ("Escape cancels, the other gestures
58950
+ * keep") for why Escape should go on meaning cancel, and for what the value
58951
+ * at open is on the picker's very first open.
58919
58952
  * @param {"close"|"cancel"|"capture"} [pointerInteractionOutsideEffect="close"]
58920
58953
  * What a click outside the popup does: close and keep ("close"), close and
58921
- * put back the value at open ("cancel"), or nothing at all ("capture").
58954
+ * put back the value at open ("cancel"), or nothing at all ("capture"). The
58955
+ * default is what gives a popup with no confirm button its way out that
58956
+ * keeps — see the same section.
58922
58957
  * @param {number|string} [marginWithContainer] Minimum gap kept between the
58923
58958
  * popup and the edges of what contains it (the viewport, or the picker's own
58924
58959
  * positioned ancestor for `popupLayer="local"`). Caps the popup's size as