@jsenv/navi 0.29.166 → 0.29.168

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.
@@ -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, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, scrollIntoViewScoped, initFocusGroup, isTouchDrivenEvent, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
+ import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, 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, isTouchDrivenEvent, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, isPressDisputedByDrag, 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";
@@ -7284,7 +7284,9 @@ const openCallout = (message, {
7284
7284
  // that whole window: by the time anything measures anchorElement, it's
7285
7285
  // already at its final position, so there's no transient frame left to
7286
7286
  // latch onto.
7287
- anchorElement.scrollIntoView({
7287
+ // Through the boxes that scroll and none of the ones that only clip: an
7288
+ // `overflow: hidden` card takes a scroll no user can give back.
7289
+ scrollIntoViewThroughScrollables(anchorElement, {
7288
7290
  behavior: "instant",
7289
7291
  block: "nearest"
7290
7292
  });
@@ -8276,13 +8278,20 @@ const formatCompactNumber = (value, lang) => {
8276
8278
  * Formats a date as a human-readable day string.
8277
8279
  *
8278
8280
  * @param {Date} date
8279
- * @param {{ lang?: string, format?: "long"|"short"|"narrow"|"numeric" }} [options]
8281
+ * @param {{ lang?: string, format?: "long"|"short"|"narrow"|"numeric"|{ weekday?: "long"|"short"|"narrow", month?: "long"|"short"|"narrow"|"numeric" } }} [options]
8282
+ * A string spells the weekday and the month the same way. An object spells
8283
+ * them apart, each key defaulting to `"long"`: a narrow card usually wants
8284
+ * the weekday whole (it is the reading anchor) and the month abbreviated (it
8285
+ * is where the characters are — "septembre" is 9 of them, "sept." reads the
8286
+ * same). `"numeric"` stays a string-only spelling: it drops the weekday and
8287
+ * writes the whole date in digits.
8280
8288
  *
8281
8289
  * @example
8282
8290
  * formatDay(new Date(), { lang: "fr" }) // "lundi 11 mai" (long, default)
8283
8291
  * formatDay(new Date(), { lang: "fr", format: "short" }) // "lun. 11 mai"
8284
8292
  * formatDay(new Date(), { lang: "fr", format: "narrow" }) // "lu. 11 mai"
8285
8293
  * formatDay(new Date(), { lang: "fr", format: "numeric" }) // "11/05/2026"
8294
+ * formatDay(new Date(), { lang: "fr", format: { weekday: "long", month: "short" } }) // "mercredi 2 sept."
8286
8295
  */
8287
8296
  const formatDay = (
8288
8297
  date,
@@ -8295,10 +8304,12 @@ const formatDay = (
8295
8304
  year: "numeric",
8296
8305
  }).format(date);
8297
8306
  }
8307
+ const { weekday = "long", month = "long" } =
8308
+ typeof format === "string" ? { weekday: format, month: format } : format;
8298
8309
  return new Intl.DateTimeFormat(lang, {
8299
- weekday: format, // "long", "short", or "narrow"
8310
+ weekday, // "long", "short", or "narrow"
8300
8311
  day: "numeric",
8301
- month: format,
8312
+ month,
8302
8313
  }).format(date);
8303
8314
  };
8304
8315
 
@@ -9909,7 +9920,7 @@ const formatDateIso = (iso, inputType) => {
9909
9920
  const locale = languagesSignal.value;
9910
9921
  if (inputType === "month") {
9911
9922
  const date = new Date(`${iso}-01T00:00:00`);
9912
- return formatMonth(date, locale);
9923
+ return formatMonth(date, { lang: locale });
9913
9924
  }
9914
9925
  // date, week, datetime-local: extract YYYY-MM-DD part and parse as local date
9915
9926
  const isoMatch = /^(\d{4}-\d{2}-\d{2})/.exec(iso);
@@ -9921,7 +9932,7 @@ const formatDateIso = (iso, inputType) => {
9921
9932
  if (isNaN(date.getTime())) {
9922
9933
  return iso;
9923
9934
  }
9924
- return formatDay(date, locale);
9935
+ return formatDay(date, { lang: locale });
9925
9936
  };
9926
9937
 
9927
9938
  const READONLY_CONSTRAINT = {
@@ -11045,6 +11056,12 @@ installImportMetaCssBuild(import.meta);/**
11045
11056
  * wants the right click to do what the hold does says so, with `contextmenu`
11046
11057
  * beside it.
11047
11058
  *
11059
+ * It DOES take the selection, and so does a swipe: nothing under either is text
11060
+ * to select, because the browser answers that same press by selecting the word
11061
+ * under the thumb and leaves it selected once its menu is gone. What never
11062
+ * answered the press keeps its text — a field, a popover, a dialog (see the
11063
+ * stylesheet below).
11064
+ *
11048
11065
  * A hold CAN open a popup while the finger is still down — a menu appearing under
11049
11066
  * a waiting finger, which is the native gesture. navi's Popover is `popover="manual"`
11050
11067
  * and owns its own dismissal, so the `pointerup` that ends the press is not read as
@@ -11129,6 +11146,14 @@ import.meta.css = [/* css */`@property --swipe-progress {
11129
11146
  -webkit-touch-callout: none;
11130
11147
  }
11131
11148
 
11149
+ [data-longpress], [data-swipe] {
11150
+ user-select: none;
11151
+ }
11152
+
11153
+ :is([data-longpress], [data-swipe]) :is([data-drag-ignore], [popover], dialog), :is([data-longpress], [data-swipe]) :is(input:not([data-press-only]), textarea), :is([data-longpress], [data-swipe]) :is([contenteditable=""], [contenteditable="true"]) {
11154
+ user-select: text;
11155
+ }
11156
+
11132
11157
  [data-swiping="left"], [data-swiping="right"] {
11133
11158
  translate: var(--swipe-pulled) 0;
11134
11159
  }
@@ -17635,7 +17660,9 @@ const useActionAsyncData = (action, {
17635
17660
  onLoad
17636
17661
  }) => {
17637
17662
  const loadingRef = useContext(LoadingContext);
17638
- if (!loadingRef) {
17663
+ // The boundary is where a suspension lands, so only the delegating path needs
17664
+ // one: `loading: true` draws the wait itself and never suspends.
17665
+ if (!loadingRef && loadingEffect !== "use") {
17639
17666
  throw new Error(`Missing <Loading>: useAsyncData delegates the wait, so it needs a <Loading> boundary above it — or "loading: true" to draw that wait here.`);
17640
17667
  }
17641
17668
  useOnLoad(action, onLoad);
@@ -17719,15 +17746,19 @@ const useActionAsyncData = (action, {
17719
17746
  }
17720
17747
  if (runningState === FAILED) {
17721
17748
  if (dismissedActionWeakSet.has(action)) {
17749
+ // Dismissing removes the error and nothing else: what is handed back is
17750
+ // the action's state without it — the data it holds, and no run in
17751
+ // progress. Holding nothing is one of those states, not a wait, so a
17752
+ // component drawing its own states gets it rather than being suspended
17753
+ // away from the retry it is offering.
17722
17754
  const staleData = action.dataSignal.peek();
17723
- if (staleData !== undefined) {
17724
- // Dismissed with stale data — return it so children render normally
17755
+ if (staleData !== undefined || loadingEffect === "use") {
17725
17756
  return [staleData, false, undefined];
17726
17757
  }
17727
- // Dismissed with no data — suspend until the action re-runs.
17728
- // A never-resolving promise would leave the component stuck forever,
17729
- // so we use an action-specific promise that resolves on RUNNING,
17730
- // which lets the component re-render and go through the normal loading path.
17758
+ // Nothing to hand over and the waits are delegated: there is no state
17759
+ // this component can draw, so it waits. The promise resolves on RUNNING
17760
+ // rather than never, so a run started from anywhere brings it back
17761
+ // through the ordinary loading path instead of leaving it stuck.
17731
17762
  let dismissedPromise = dismissedActionPendingPromiseWeakMap.get(action);
17732
17763
  if (!dismissedPromise) {
17733
17764
  dismissedPromise = new Promise(resolve => {
@@ -17756,7 +17787,10 @@ const useActionAsyncData = (action, {
17756
17787
  dismissedActionWeakSet.add(action);
17757
17788
  setTick(n => n + 1);
17758
17789
  };
17759
- return [undefined, false, actionError, dismissError];
17790
+ // The previous answer is still there and a failed refresh does not unmake
17791
+ // it, so both are handed over: the component can say the failure over the
17792
+ // content it already has instead of choosing between the two.
17793
+ return [action.dataSignal.peek(), false, actionError, dismissError];
17760
17794
  }
17761
17795
  // Not marked: nothing is displayed yet — the boundary that catches this is
17762
17796
  // what says so, and only if it has something to show.
@@ -18916,9 +18950,15 @@ const getHowToHandleStyleProp = (name) => {
18916
18950
  }
18917
18951
  return getStyle;
18918
18952
  };
18919
- const prepareStyleValue = (existingValue, value, name, styleContext) => {
18953
+ // The css value `name` would take, written in the scale that style is read in:
18954
+ // "s" on a spacing style is a length, "s" on a typo style is a font size, and a
18955
+ // bare number gets the unit the style expects.
18956
+ const stringifyStyleValue = (value, name, styleContext) => {
18920
18957
  const stringifier = getStringifier(name);
18921
- const cssValue = stringifier(value, name, styleContext);
18958
+ return stringifier(value, name, styleContext);
18959
+ };
18960
+ const prepareStyleValue = (existingValue, value, name, styleContext) => {
18961
+ const cssValue = stringifyStyleValue(value, name, styleContext);
18922
18962
  const mergedValue = mergeOneStyle(existingValue, cssValue, name, "css");
18923
18963
  return mergedValue;
18924
18964
  };
@@ -20954,6 +20994,15 @@ import.meta.css = [/* css */`[data-scrollable] > .navi_loading_outline_wrapper,
20954
20994
  const PSEUDO_CLASSES_DEFAULT = [];
20955
20995
  const PSEUDO_ELEMENTS_DEFAULT = [];
20956
20996
  const STYLE_CSS_VARS_DEFAULT = {};
20997
+ // An entry of styleCSSVars is the css variable the prop writes into, alone when
20998
+ // the prop is named after a css style — Box then already knows how to read its
20999
+ // value — or paired with the style whose values it borrows when it is not.
21000
+ const readCSSVarEntry = entry => {
21001
+ if (Array.isArray(entry)) {
21002
+ return entry;
21003
+ }
21004
+ return [entry, null];
21005
+ };
20957
21006
  // When only pseudoStateSelector is set (no visualSelector), the box owns its
20958
21007
  // visual identity. Only event handlers and these explicit props are forwarded
20959
21008
  // to the inner semantic/interactive child element.
@@ -20964,7 +21013,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
20964
21013
  * as?: string,
20965
21014
  * className?: string,
20966
21015
  * style?: import("ignore:preact").JSX.CSSProperties & { [pseudo: string]: import("ignore:preact").JSX.CSSProperties },
20967
- * styleCSSVars?: { [stylePropName: string]: string },
21016
+ * styleCSSVars?: { [propName: string]: string | [string, string] },
20968
21017
  * inline?: boolean,
20969
21018
  * block?: boolean,
20970
21019
  * flex?: "x" | "y" | boolean,
@@ -21325,7 +21374,7 @@ const computeBox = (props, parentBoxFlow) => {
21325
21374
  const canForwardToChild = hasChildUsingForwardedProps;
21326
21375
  const addStyle = (value, name, styleContext, stylesTarget) => {
21327
21376
  const mergedValue = prepareStyleValue(stylesTarget[name], value, name, styleContext);
21328
- const cssVar = styleContext.styleCSSVars[name];
21377
+ const [cssVar] = readCSSVarEntry(styleContext.styleCSSVars[name]);
21329
21378
  if (cssVar) {
21330
21379
  addCSSVar(mergedValue, cssVar, stylesTarget);
21331
21380
  if (name === "borderRadius" && value === "inherit") {
@@ -21446,10 +21495,18 @@ const computeBox = (props, parentBoxFlow) => {
21446
21495
  }
21447
21496
  return;
21448
21497
  }
21449
- const cssVarName = styleCSSVars[name];
21450
- if (cssVarName) {
21498
+ const cssVarEntry = styleCSSVars[name];
21499
+ if (cssVarEntry) {
21451
21500
  if (value !== undefined) {
21452
- addCSSVar(value, cssVarName, boxStylesTarget);
21501
+ const [cssVarName, valueStyleName] = readCSSVarEntry(cssVarEntry);
21502
+ const cssValue = valueStyleName ? stringifyStyleValue(value, valueStyleName, styleContext) : value;
21503
+ if (isSizeSpacingKey(cssValue)) {
21504
+ // A size keyword reaching a custom property stays that word: every
21505
+ // declaration reading the variable is then invalid, drops back to
21506
+ // its initial value, and says nothing about it.
21507
+ console.warn(`"${name}" cannot take the size keyword "${cssValue}": it goes into ${cssVarName} as-is, which makes every declaration reading that variable invalid. Pass "${stringifySpacingStyle(cssValue)}" instead.`);
21508
+ }
21509
+ addCSSVar(cssValue, cssVarName, boxStylesTarget);
21453
21510
  }
21454
21511
  return;
21455
21512
  }
@@ -31244,7 +31301,32 @@ const anyMatchingRouteSignal = (routes) => {
31244
31301
  * when that is not the element asking: a menu opened by a press belongs at the
31245
31302
  * point the press happened, and the row that was pressed is not that point.
31246
31303
  */
31247
- const triggerNaviCommand = (
31304
+ const triggerNaviCommand = (element, command, event, options) => {
31305
+ const run = resolveNaviCommand(element, command, event, options);
31306
+ return run ? run() : false;
31307
+ };
31308
+
31309
+ /**
31310
+ * The same command, in its two moments: WHAT it aims at, decided here and now,
31311
+ * and what it then does, in the function returned. One caller needs them apart
31312
+ * — a button whose command waits for its own action (see the command trigger in
31313
+ * ui_state_controller.js) — and it needs it because resolution reads the DOM
31314
+ * AROUND the source: the closest expandable, the parent control, the slide it
31315
+ * stands in. An action that succeeds has usually re-rendered the tree it was
31316
+ * asked from, which is what an action is for, so by then the source is
31317
+ * detached: `closest` walks a subtree the popup is no longer in, and a command
31318
+ * that had a perfectly good target at the press finds none and is dropped with
31319
+ * a warning — the popup stays open over a request that went through.
31320
+ *
31321
+ * Freezing the target at the press is also what one means by it: the popup
31322
+ * pressed in is the popup to close, even when the press is what emptied it.
31323
+ * What the command DOES stays live — every handler reads the state it needs
31324
+ * inside its implementation, never while resolving.
31325
+ *
31326
+ * Returns undefined when there is nothing to run (unknown command, no target).
31327
+ * See triggerNaviCommand's jsdoc for the parameters.
31328
+ */
31329
+ const resolveNaviCommand = (
31248
31330
  element,
31249
31331
  command,
31250
31332
  event,
@@ -31259,7 +31341,7 @@ const triggerNaviCommand = (
31259
31341
  NAVI_COMMANDS[command] || NAVI_COMMANDS[commandName(command)];
31260
31342
  if (!naviCommand) {
31261
31343
  console.warn(`Unknown command "${command}"`);
31262
- return false;
31344
+ return undefined;
31263
31345
  }
31264
31346
  // Check for explicit HTML target overrides early so a misconfigured commandfor
31265
31347
  // attribute (id not found) aborts immediately rather than silently falling back
@@ -31267,7 +31349,7 @@ const triggerNaviCommand = (
31267
31349
  const explicitTarget = resolveExplicitTarget(element);
31268
31350
  if (explicitTarget === null) {
31269
31351
  // attribute was present but target not found — already warned inside resolveExplicitTarget
31270
- return false;
31352
+ return undefined;
31271
31353
  }
31272
31354
  const execute = naviCommand.commandHandler(element, event, {
31273
31355
  // Whatever followed the colon: "--navi-go-to-slide:edit" → "edit".
@@ -31280,37 +31362,40 @@ const triggerNaviCommand = (
31280
31362
  });
31281
31363
  if (!execute) {
31282
31364
  if (optional) {
31283
- return false;
31365
+ return undefined;
31284
31366
  }
31285
31367
  console.warn(
31286
31368
  `"${command}" triggered on element but no suitable target found`,
31287
31369
  element,
31288
31370
  );
31289
- return false;
31371
+ return undefined;
31290
31372
  }
31291
31373
  const { target, implementation } = execute;
31292
- // The event is how the target takes part in what it was asked to do (a popup
31293
- // closing on --navi-close also has its own things to do); the command itself
31294
- // is the implementation, and it must not depend on someone listening. Nobody
31295
- // does when the target has left the document — a trigger whose own action
31296
- // took it away, which is the shape of "delete this, then leave the page it
31297
- // was on" (see runWhenActionSucceeded in rules/control_action.js): the
31298
- // command is decided while the trigger is there and runs once the action has
31299
- // succeeded, by which time the row it stood in is gone. So the command runs
31300
- // here rather than being lost in silence.
31301
- const detail = {
31302
- command,
31303
- event,
31304
- source: element,
31305
- implementation,
31306
- value,
31307
- answered: false,
31374
+ return () => {
31375
+ // The event is how the target takes part in what it was asked to do (a
31376
+ // popup closing on --navi-close also has its own things to do); the command
31377
+ // itself is the implementation, and it must not depend on someone
31378
+ // listening. Nobody does when the target has left the document — a trigger
31379
+ // whose own action took it away, which is the shape of "delete this, then
31380
+ // leave the page it was on" (see runWhenActionSucceeded in
31381
+ // rules/control_action.js): the command was decided above, while the
31382
+ // trigger was there, and runs once the action has succeeded, by which time
31383
+ // the row it stood in is gone. So the command runs here rather than being
31384
+ // lost in silence.
31385
+ const detail = {
31386
+ command,
31387
+ event,
31388
+ source: element,
31389
+ implementation,
31390
+ value,
31391
+ answered: false,
31392
+ };
31393
+ const dispatched = dispatchCustomEvent(target, "navi_command", detail);
31394
+ if (!detail.answered) {
31395
+ implementation();
31396
+ }
31397
+ return dispatched;
31308
31398
  };
31309
- const dispatched = dispatchCustomEvent(target, "navi_command", detail);
31310
- if (!detail.answered) {
31311
- implementation();
31312
- }
31313
- return dispatched;
31314
31399
  };
31315
31400
 
31316
31401
  // Returns the target explicitly declared via HTML attributes (commandfor / navi-command-target),
@@ -33722,18 +33807,26 @@ const useUIStateController = (
33722
33807
  debugUIState(
33723
33808
  `triggering command "${command}" for "${controlType}"`,
33724
33809
  );
33725
- const runCommand = () => {
33726
- triggerNaviCommand(element, command, e);
33727
- };
33728
33810
  // What the press means may not be due yet: a button with an
33729
33811
  // action of its own runs the work first and lets its command
33730
33812
  // follow only once that work succeeded (see control_hooks).
33731
33813
  // The command is handed over rather than run; nobody claiming
33732
33814
  // it means now.
33815
+ //
33816
+ // Only the RUN waits. What the command aims at is decided here,
33817
+ // while the button is still in the document, because that is
33818
+ // what resolving reads — the popup around it, the control above
33819
+ // it — and the action it waits for is likely to re-render that
33820
+ // away before it succeeds. Resolved late, the command would
33821
+ // find nothing to aim at and be dropped (see
33822
+ // resolveNaviCommand).
33733
33823
  if (controller.commandDeferral) {
33734
- controller.commandDeferral(runCommand);
33824
+ const runCommand = resolveNaviCommand(element, command, e);
33825
+ if (runCommand) {
33826
+ controller.commandDeferral(runCommand);
33827
+ }
33735
33828
  } else {
33736
- runCommand();
33829
+ triggerNaviCommand(element, command, e);
33737
33830
  }
33738
33831
  }
33739
33832
  }
@@ -36346,6 +36439,14 @@ const useControlProps = (props, {
36346
36439
  return false;
36347
36440
  }
36348
36441
  const control = ref.current;
36442
+ if (!control) {
36443
+ // The ui action the reaction ran a line earlier is free to take this
36444
+ // control away: a list row answering its popup's question closes it, and
36445
+ // mount="while-opened" unmounts the row. Nothing left to read a value
36446
+ // from, and what the press was for has already happened (the button and
36447
+ // link reactions bail on the same ground).
36448
+ return false;
36449
+ }
36349
36450
  const currentValue = readControlValue(control);
36350
36451
  // For checkables: skip value dedup. The browser only fires `input` when state
36351
36452
  // actually changes, so there is no spurious double-dispatch to guard against.
@@ -39522,8 +39623,19 @@ const TextAnchor = ({
39522
39623
  // which reads as the child "jumping" even though nothing about its own
39523
39624
  // geometry changed. useDisplayedLayoutEffect skips the initial run in
39524
39625
  // that case and reruns once the ancestor actually opens instead.
39525
- useDisplayedLayoutEffect(anchorRef, anchorEl => {
39526
- const childEl = childRef.current;
39626
+ //
39627
+ // It is asked about the CHILD, not about the anchor: the anchor is this
39628
+ // effect's own measuring device, displayed or not according to what the
39629
+ // effect last decided (see the hidden attribute below), so asking whether
39630
+ // it is on screen answers with that decision rather than with a fact about
39631
+ // the screen. On the façade of something closed — an icon in a picker's
39632
+ // trigger, where the openable ancestor is the picker itself — that answer
39633
+ // is "hidden", the correction waits for the popup to open, and the icon
39634
+ // jumps half a pixel on the first click. The child is what is being
39635
+ // positioned, and it is on screen exactly when the correction means
39636
+ // something.
39637
+ useDisplayedLayoutEffect(childRef, childEl => {
39638
+ const anchorEl = anchorRef.current;
39527
39639
  if (!anchorEl || !childEl) {
39528
39640
  return;
39529
39641
  }
@@ -41884,7 +41996,7 @@ const NavStyleCSSVars = {
41884
41996
  paddingLeft: "--nav-padding-left",
41885
41997
  background: "--nav-background",
41886
41998
  currentIndicatorColor: "--nav-current-indicator-color",
41887
- currentIndicatorSize: "--nav-current-indicator-size"
41999
+ currentIndicatorSize: ["--nav-current-indicator-size", "height"]
41888
42000
  };
41889
42001
  const positionOfCurrentIndicator = (currentIndicator, vertical) => {
41890
42002
  if (currentIndicator === true) {
@@ -42979,7 +43091,7 @@ const css$V = /* css */`@layer navi {
42979
43091
  const BinderStyleCSSVars = {
42980
43092
  borderWidth: "--binder-border-width",
42981
43093
  borderRadius: "--binder-border-radius",
42982
- tabBorderRadius: "--binder-tab-border-radius",
43094
+ tabBorderRadius: ["--binder-tab-border-radius", "borderRadius"],
42983
43095
  borderColor: "--binder-border-color",
42984
43096
  background: "--binder-background",
42985
43097
  tabBackground: "--binder-tab-background",
@@ -42988,7 +43100,7 @@ const BinderStyleCSSVars = {
42988
43100
  tabColor: "--binder-tab-color",
42989
43101
  paddingX: "--binder-padding-x",
42990
43102
  paddingY: "--binder-padding-y",
42991
- pagePadding: "--binder-page-padding"
43103
+ pagePadding: ["--binder-page-padding", "padding"]
42992
43104
  };
42993
43105
  const TABS_ALIGN_TO_JUSTIFY_CONTENT = {
42994
43106
  start: "flex-start",
@@ -43139,12 +43251,12 @@ const Binder = ({
43139
43251
  "data-tabs-position": tabsPosition,
43140
43252
  "data-tabs-align": tabsAlign,
43141
43253
  "data-scrollable-page": scrollablePage ? "" : undefined,
43142
- borderWidth: withPixelUnit(borderWidth),
43143
- borderRadius: withPixelUnit(borderRadius),
43144
- tabBorderRadius: withPixelUnit(tabBorderRadius),
43145
- paddingX: withPixelUnit(paddingX),
43146
- paddingY: withPixelUnit(paddingY),
43147
- pagePadding: withPixelUnit(pagePadding),
43254
+ borderWidth: borderWidth,
43255
+ borderRadius: borderRadius,
43256
+ tabBorderRadius: tabBorderRadius,
43257
+ paddingX: paddingX,
43258
+ paddingY: paddingY,
43259
+ pagePadding: pagePadding,
43148
43260
  ...props,
43149
43261
  styleCSSVars: BinderStyleCSSVars,
43150
43262
  children: [jsx(BinderOutline, {
@@ -43377,15 +43489,6 @@ const measureDrawing = ({
43377
43489
  };
43378
43490
  };
43379
43491
 
43380
- // A bare number in a CSS var stays a bare number and the declaration is
43381
- // dropped; every length prop here is in pixels when it is not spelled out.
43382
- const withPixelUnit = value => {
43383
- if (typeof value === "number") {
43384
- return `${value}px`;
43385
- }
43386
- return value;
43387
- };
43388
-
43389
43492
  /**
43390
43493
  * How much room the fixed bars take on each edge, published for the safe area
43391
43494
  * to add up (see layout/safe_area.js — it declares the four variables written
@@ -48101,14 +48204,13 @@ const FieldAsLabel = props => {
48101
48204
  });
48102
48205
  };
48103
48206
  const FieldCSSVars = {
48104
- spacingWithControl: "--spacing-with-control"
48207
+ spacingWithControl: ["--spacing-with-control", "padding"]
48105
48208
  };
48106
48209
  const FieldAsContainer = props => {
48107
48210
  import.meta.css = [css$L, "@jsenv/navi/src/control/field.jsx"];
48108
48211
  const {
48109
48212
  children
48110
48213
  } = props;
48111
- props.spacingWithControl = resolveSpacingSize(props.spacingWithControl);
48112
48214
  const isVertical = props.flex === "y";
48113
48215
  const [messageProps, remainingProps] = extractMessageAndRemainingProps({
48114
48216
  ...props,
@@ -50222,10 +50324,6 @@ const useInputTextualProps = props => {
50222
50324
  };
50223
50325
  const InputTextualUI = props => {
50224
50326
  installInputCss();
50225
- // Spacing props travel to CSS as a raw custom property value, so the size
50226
- // keywords have to become lengths here — "s" reaching CSS untouched makes the
50227
- // declaration invalid, silently, and the gap just goes away.
50228
- props.slotSpacing = resolveSpacingSize(props.slotSpacing);
50229
50327
  const {
50230
50328
  ui,
50231
50329
  variant,
@@ -50430,7 +50528,7 @@ const RealInput = ({
50430
50528
  // Shared with textarea.jsx: a textarea is styled as a .navi_input box, so the
50431
50529
  // two read the same style props and pseudo states.
50432
50530
  const InputStyleCSSVars = {
50433
- "slotSpacing": "--slot-spacing",
50531
+ "slotSpacing": ["--slot-spacing", "margin"],
50434
50532
  "outlineWidth": "--outline-width",
50435
50533
  "borderWidth": "--border-width",
50436
50534
  "borderRadius": "--border-radius",
@@ -51068,6 +51166,10 @@ const css$I = /* css */`.navi_textarea {
51068
51166
  }
51069
51167
  }
51070
51168
 
51169
+ &[data-width-from-container] {
51170
+ contain: inline-size;
51171
+ }
51172
+
51071
51173
  &[data-resizable] {
51072
51174
  & .navi_control_input {
51073
51175
  height: calc(var(--textarea-min-rows, 1.5) * 1lh);
@@ -51118,9 +51220,13 @@ const TextareaStyleCSSVars = {
51118
51220
  * @param {number} [maxLength] The character limit, validated at submit. Pair
51119
51221
  * with `maxLengthGuard` to block typing past it, and render a
51120
51222
  * TextareaCharCount to show it.
51121
- * @param {string} [width="35ch"] The control's width. Fixed on purpose: with
51122
- * field-sizing the width would otherwise follow the longest line, and a box
51123
- * that widens while one types is a box one chases.
51223
+ * @param {string} [width="35ch"] The control's width. A length by default, on
51224
+ * purpose: with field-sizing the width would otherwise follow the longest
51225
+ * line, and a box that widens while one types is a box one chases. A
51226
+ * percentage means "fill what contains me" and holds the same promise — the
51227
+ * control then takes the width it is given without having a say in it, even
51228
+ * inside a container that sizes itself from its content (a Popover, a
51229
+ * Picker's popup), where the text wraps instead of widening the popup.
51124
51230
  */
51125
51231
  const Textarea = ({
51126
51232
  // Destructured, never deleted off the props object: Preact reuses the same
@@ -51147,8 +51253,21 @@ const Textarea = ({
51147
51253
  // element they would become its text content.
51148
51254
  delete hostProps.children;
51149
51255
  const loading = basePseudoState[":-navi-loading"];
51256
+
51257
+ // A percentage is not a definite length: while a container is being sized
51258
+ // from its content it cannot be resolved, so the box falls back to its own
51259
+ // max-content — which `field-sizing: content` makes the longest line typed,
51260
+ // and the field widens the very box it asked to fill. The CSS above takes
51261
+ // that vote away, which is why the percentage lands on the contained box
51262
+ // rather than on the field: the field fills it from the inside (flex-grow
51263
+ // in input_css.js).
51264
+ const widthFromContainer = String(width).includes("%");
51150
51265
  delete rootProps.width;
51151
- hostProps.width = width;
51266
+ if (widthFromContainer) {
51267
+ rootProps.width = width;
51268
+ } else {
51269
+ hostProps.width = width;
51270
+ }
51152
51271
  return jsxs(Box, {
51153
51272
  as: "span",
51154
51273
  inline: true,
@@ -51157,6 +51276,7 @@ const Textarea = ({
51157
51276
  ...rootProps,
51158
51277
  basePseudoState: basePseudoState,
51159
51278
  "data-resizable": resizable ? "" : undefined,
51279
+ "data-width-from-container": widthFromContainer ? "" : undefined,
51160
51280
  styleCSSVars: TextareaStyleCSSVars,
51161
51281
  pseudoStateSelector: ".navi_control_input",
51162
51282
  pseudoClasses: InputPseudoClasses,
@@ -68781,16 +68901,6 @@ const PickerButton = props => {
68781
68901
  if (typeof props.maxLines === "string") {
68782
68902
  props.maxLines = parseInt(props.maxLines);
68783
68903
  }
68784
- // Spacing props travel to CSS as a raw custom property value, so the size
68785
- // keywords have to become lengths here — "s" reaching CSS untouched makes
68786
- // the declaration invalid, silently, and the gap just goes away.
68787
- props.slotSpacing = resolveSpacingSize(props.slotSpacing);
68788
- for (const pressPaddingPropName of PRESS_PADDING_PROP_NAMES) {
68789
- const pressPadding = props[pressPaddingPropName];
68790
- if (pressPadding !== undefined) {
68791
- props[pressPaddingPropName] = stringifySpacingStyle(pressPadding);
68792
- }
68793
- }
68794
68904
  const {
68795
68905
  ref,
68796
68906
  variant,
@@ -69388,32 +69498,29 @@ const NON_MOBILE_KEYBOARD_TYPES = new Set(["date", "month", "week", "time", "dat
69388
69498
  const PICKER_BUTTON_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":focus-within", ":read-only", ":disabled", ":-navi-loading", ":-navi-expanded", ":-navi-has-value"];
69389
69499
  const PickerInputPseudoClasses = [":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading", ":-navi-has-value", ":-navi-expanded"];
69390
69500
 
69391
- // Spacing props that are not the names of real CSS styles, so Box hands them to
69392
- // the custom property untouched: a size keyword would reach CSS as the word "s"
69393
- // and a number as a unitless one, either of which makes the calc() using it
69394
- // invalid and drops the press area back to the box. Hence the pass in
69395
- // PickerButton, which turns them into lengths.
69396
- const PRESS_PADDING_PROP_NAMES = ["pressPadding", "pressPaddingX", "pressPaddingY", "pressPaddingTop", "pressPaddingRight", "pressPaddingBottom", "pressPaddingLeft"];
69501
+ // A pair is [the css variable, the style whose values the prop is written in]:
69502
+ // popupBorderRadius takes what borderRadius takes, dialogMinWidth what minWidth
69503
+ // takes — see box.jsx's readCSSVarEntry.
69397
69504
  const PickerStyleCSSVars = {
69398
69505
  "outlineWidth": "--picker-outline-width",
69399
69506
  "borderWidth": "--picker-border-width",
69400
69507
  "borderRadius": "--picker-border-radius",
69401
- "popoverMaxHeight": "--picker-popover-max-height",
69402
- "dialogMinWidth": "--picker-dialog-min-width",
69403
- "dialogMinHeight": "--picker-dialog-min-height",
69404
- "dialogMaxWidth": "--picker-dialog-max-width",
69405
- "dialogMaxHeight": "--picker-dialog-max-height",
69508
+ "popoverMaxHeight": ["--picker-popover-max-height", "maxHeight"],
69509
+ "dialogMinWidth": ["--picker-dialog-min-width", "minWidth"],
69510
+ "dialogMinHeight": ["--picker-dialog-min-height", "minHeight"],
69511
+ "dialogMaxWidth": ["--picker-dialog-max-width", "maxWidth"],
69512
+ "dialogMaxHeight": ["--picker-dialog-max-height", "maxHeight"],
69406
69513
  "popupBackgroundColor": "--picker-popup-background-color",
69407
- "popupBorderRadius": "--picker-popup-border-radius",
69408
- "dialogBorderWidth": "--picker-dialog-border-width",
69409
- "slotSpacing": "--picker-slot-spacing",
69410
- "pressPadding": "--picker-press-padding",
69411
- "pressPaddingX": "--picker-press-padding-x",
69412
- "pressPaddingY": "--picker-press-padding-y",
69413
- "pressPaddingTop": "--picker-press-padding-top",
69414
- "pressPaddingRight": "--picker-press-padding-right",
69415
- "pressPaddingBottom": "--picker-press-padding-bottom",
69416
- "pressPaddingLeft": "--picker-press-padding-left",
69514
+ "popupBorderRadius": ["--picker-popup-border-radius", "borderRadius"],
69515
+ "dialogBorderWidth": ["--picker-dialog-border-width", "borderWidth"],
69516
+ "slotSpacing": ["--picker-slot-spacing", "margin"],
69517
+ "pressPadding": ["--picker-press-padding", "padding"],
69518
+ "pressPaddingX": ["--picker-press-padding-x", "padding"],
69519
+ "pressPaddingY": ["--picker-press-padding-y", "padding"],
69520
+ "pressPaddingTop": ["--picker-press-padding-top", "padding"],
69521
+ "pressPaddingRight": ["--picker-press-padding-right", "padding"],
69522
+ "pressPaddingBottom": ["--picker-press-padding-bottom", "padding"],
69523
+ "pressPaddingLeft": ["--picker-press-padding-left", "padding"],
69417
69524
  // alignX/alignY resolve to these two on a flex-x box; naming the CSS style
69418
69525
  // (not the prop) is what styleCSSVars matches, so justifyContent/alignItems
69419
69526
  // passed directly land in the same variables.