@jsenv/navi 0.29.323 → 0.29.324

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.
@@ -7,7 +7,7 @@ export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
7
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, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, 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, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
10
- import { isValidElement, createContext, render, h, toChildArray, options, cloneElement, Fragment as Fragment$1 } from "preact";
10
+ import { isValidElement, createContext, render, h, toChildArray, options, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
11
11
  import { useErrorBoundary, useLayoutEffect, useContext, useCallback, useRef, useState, useEffect, useMemo, useId } from "preact/hooks";
12
12
  import { humanizeI18n, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
13
13
  export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
@@ -31648,8 +31648,26 @@ const resolveExplicitTarget = (element) => {
31648
31648
  }
31649
31649
  return undefined;
31650
31650
  };
31651
+ // The control around the source that a command can be about: one holding a
31652
+ // value of its own. A popup is a control root too (it answers interactions,
31653
+ // carries a callout), but it has no host of its own — asked for one, it
31654
+ // answers with the first control INSIDE it, which for a field sending
31655
+ // --navi-update from a picker's popup is that very field: the command then
31656
+ // sets the field to what it already holds, which fires the command again,
31657
+ // without end. Such a surface is walked past, up to the picker or the form.
31651
31658
  const resolveFirstParentControl = (el) => {
31652
- return getParentControl(el);
31659
+ let parentControl = getParentControl(el);
31660
+ while (parentControl && !ownsControlHost(parentControl)) {
31661
+ parentControl = getParentControl(parentControl);
31662
+ }
31663
+ return parentControl;
31664
+ };
31665
+ const ownsControlHost = (controlRoot) => {
31666
+ const controlHost = findControlHost(controlRoot);
31667
+ return (
31668
+ Boolean(controlHost) &&
31669
+ controlHost.closest("[navi-control]") === controlRoot
31670
+ );
31653
31671
  };
31654
31672
  const resolveFirstChildControl = (el) => {
31655
31673
  let startEl;
@@ -34400,6 +34418,21 @@ const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
34400
34418
  */
34401
34419
  const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
34402
34420
 
34421
+ /**
34422
+ * `data-navi-popup-inside` is the other direction: a box of the PAGE the
34423
+ * caller says belongs to a popup, so a press on it is not outside that popup
34424
+ * even though it lies beyond the border box — a card whose press is what
34425
+ * fills the one panel of a board. Its value names the popup by `id`, the way
34426
+ * `commandfor` does (several ids, space separated), because the exemption is
34427
+ * one popup's: a menu open beside the board still closes on the same press.
34428
+ *
34429
+ * Unlike the outside marker it answers for its whole subtree: the outside
34430
+ * marker carves free space out of a surface whose controls stay surface, this
34431
+ * one adds a whole thing — the card, its count, its buttons — to the popup's
34432
+ * own ground, and nothing in it is a dismissal.
34433
+ */
34434
+ const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
34435
+
34403
34436
  /**
34404
34437
  * What a press landing on a region the caller declared as not-its-surface
34405
34438
  * does: exactly what the same press on the backdrop would do.
@@ -34524,6 +34557,14 @@ const armOutsidePressClose = (
34524
34557
  ) {
34525
34558
  return;
34526
34559
  }
34560
+ if (
34561
+ popupEl.id &&
34562
+ pointerDownEvent.target.closest?.(
34563
+ `[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupEl.id)}"]`,
34564
+ )
34565
+ ) {
34566
+ return;
34567
+ }
34527
34568
  // Real DOM containment wins over the rectangle below — an element
34528
34569
  // genuinely inside the popup (`overflow: visible`, a negative margin, an
34529
34570
  // absolutely-positioned child) can be painted outside its border box, and
@@ -47792,33 +47833,6 @@ const createToggleEvent = open => {
47792
47833
  return toggleEvent;
47793
47834
  };
47794
47835
 
47795
- const NextResolverContext = createContext(null);
47796
- const useNextResolver = () => useContext(NextResolverContext);
47797
-
47798
- /**
47799
- * Creates a renderComponent function that passes props through a chain of resolvers.
47800
- * Each resolver is a Preact component rendered in sequence (hooks are allowed).
47801
- * To pass through to the next resolver, call useNextResolver() and render the
47802
- * returned Next component with the desired props.
47803
- * To terminate the chain early (e.g. render a specialized component), render
47804
- * directly without calling Next.
47805
- *
47806
- * The last entry in the array is the final/target component — it receives null
47807
- * from useNextResolver() indicating it is terminal.
47808
- *
47809
- * Usage:
47810
- * const renderButton = createComponentResolver([ResolverA, ResolverB, ButtonTarget]);
47811
- * // Then inside a component render:
47812
- * renderButton(props)
47813
- *
47814
- * Each position of the chain has a runner of its own, defined once: it renders
47815
- * its resolver under a NextResolverContext holding the runner after it. A
47816
- * resolver that re-renders on its own and renders <Next> therefore resumes at
47817
- * its own position without any bookkeeping — the Next it was given is the one
47818
- * made for it. A chain rendered by the hundred (a list's rows) pays two
47819
- * components per position, the runner and the resolver, so nothing here is a
47820
- * component that merely forwards.
47821
- */
47822
47836
  const createComponentResolver = (resolvers, {
47823
47837
  pure
47824
47838
  } = {}) => {
@@ -47827,13 +47841,10 @@ const createComponentResolver = (resolvers, {
47827
47841
  for (let index = 0; index < resolvers.length; index++) {
47828
47842
  const Resolver = resolvers[index];
47829
47843
  const isLast = index === lastIndex;
47830
- const Runner = props => jsx(NextResolverContext.Provider, {
47831
- value: isLast ? null : runners[index + 1],
47832
- children: jsx(Resolver, {
47833
- ...props
47834
- })
47835
- });
47836
- Runner.displayName = `${Resolver.displayName || Resolver.name}Runner`;
47844
+ function Runner(props) {
47845
+ return runResolverBody(this, Resolver, props, isLast ? null : runners[index + 1]);
47846
+ }
47847
+ Runner.displayName = Resolver.displayName || Resolver.name;
47837
47848
  runners.push(Runner);
47838
47849
  }
47839
47850
  const FirstRunner = runners[0];
@@ -47864,6 +47875,54 @@ const createComponentResolver = (resolvers, {
47864
47875
  }
47865
47876
  return PureRenderComponent;
47866
47877
  };
47878
+ const useNextResolver = () => {
47879
+ if (currentNext === OUTSIDE_RESOLVER_BODY) {
47880
+ throw new Error("useNextResolver() called outside a resolver body: a resolver rendered by another resolver is rendered with renderResolver(Resolver, props), not as a JSX child.");
47881
+ }
47882
+ return currentNext;
47883
+ };
47884
+ const renderResolver = (Resolver, props) => {
47885
+ if (currentNext === OUTSIDE_RESOLVER_BODY) {
47886
+ throw new Error(`renderResolver(${Resolver.displayName || Resolver.name}) called outside a resolver body: only a resolver can render another one.`);
47887
+ }
47888
+ let SubRunner = subRunnerByResolver.get(Resolver);
47889
+ if (!SubRunner) {
47890
+ SubRunner = function (props) {
47891
+ const {
47892
+ [NEXT_PROP]: next,
47893
+ ...ownProps
47894
+ } = props;
47895
+ return runResolverBody(this, Resolver, ownProps, next);
47896
+ };
47897
+ SubRunner.displayName = Resolver.displayName || Resolver.name;
47898
+ subRunnerByResolver.set(Resolver, SubRunner);
47899
+ }
47900
+ return createElement(SubRunner, {
47901
+ ...props,
47902
+ [NEXT_PROP]: currentNext
47903
+ });
47904
+ };
47905
+
47906
+ // What useNextResolver() answers: the runner after the one whose body is
47907
+ // running, null for the last — and, outside any body, a value that is neither.
47908
+ const OUTSIDE_RESOLVER_BODY = Symbol("outside_resolver_body");
47909
+ let currentNext = OUTSIDE_RESOLVER_BODY;
47910
+ const runResolverBody = (instance, Resolver, props, next) => {
47911
+ const nextBefore = currentNext;
47912
+ currentNext = next;
47913
+ try {
47914
+ return Resolver.call(instance, props);
47915
+ } finally {
47916
+ currentNext = nextBefore;
47917
+ }
47918
+ };
47919
+
47920
+ // One runner per resolver rendered through renderResolver, made on first use:
47921
+ // the same component type on every render, so a resolver picked again is
47922
+ // updated in place and another one picked is a remount, like any child.
47923
+ const subRunnerByResolver = new WeakMap();
47924
+ // Rides on the props of a sub-runner, stripped before the body sees them.
47925
+ const NEXT_PROP = "navi-resolver-next";
47867
47926
  function pureShouldComponentUpdate(nextProps) {
47868
47927
  return shallowDiffers(this.props, nextProps);
47869
47928
  }
@@ -48689,9 +48748,7 @@ const ButtonConfirm = ({
48689
48748
  const ButtonRouteResolver = props => {
48690
48749
  const Next = useNextResolver();
48691
48750
  if (props.route) {
48692
- return jsx(ButtonWithRoute, {
48693
- ...props
48694
- });
48751
+ return renderResolver(ButtonWithRoute, props);
48695
48752
  }
48696
48753
  return jsx(Next, {
48697
48754
  ...props
@@ -51289,9 +51346,7 @@ const installInputCss = () => {
51289
51346
  const InputModeResolver = props => {
51290
51347
  const Next = useNextResolver();
51291
51348
  if (props.inputMode === "numeric" || props.inputMode === "decimal") {
51292
- return jsx(InputModeNumericOrDecimal, {
51293
- ...props
51294
- });
51349
+ return renderResolver(InputModeNumericOrDecimal, props);
51295
51350
  }
51296
51351
  return jsx(Next, {
51297
51352
  ...props
@@ -51519,39 +51574,25 @@ const InputTypeResolver = props => {
51519
51574
  // very same "icon while empty, clear button once filled" affordance without
51520
51575
  // pretending to be a search box.
51521
51576
  if (props.clearable && props.type !== "search") {
51522
- return jsx(InputClearable, {
51523
- ...props
51524
- });
51577
+ return renderResolver(InputClearable, props);
51525
51578
  }
51526
51579
  if (props.type === "search") {
51527
- return jsx(InputSearch, {
51528
- ...props
51529
- });
51580
+ return renderResolver(InputSearch, props);
51530
51581
  }
51531
51582
  if (props.type === "email") {
51532
- return jsx(InputEmail, {
51533
- ...props
51534
- });
51583
+ return renderResolver(InputEmail, props);
51535
51584
  }
51536
51585
  if (props.type === "tel") {
51537
- return jsx(InputTel, {
51538
- ...props
51539
- });
51586
+ return renderResolver(InputTel, props);
51540
51587
  }
51541
51588
  if (props.type === "number") {
51542
- return jsx(InputNumber, {
51543
- ...props
51544
- });
51589
+ return renderResolver(InputNumber, props);
51545
51590
  }
51546
51591
  if (props.type === "color") {
51547
- return jsx(InputColor, {
51548
- ...props
51549
- });
51592
+ return renderResolver(InputColor, props);
51550
51593
  }
51551
51594
  if (props.type === "datetime-local") {
51552
- return jsx(InputDatetimeLocal, {
51553
- ...props
51554
- });
51595
+ return renderResolver(InputDatetimeLocal, props);
51555
51596
  }
51556
51597
  return jsx(Next, {
51557
51598
  ...props
@@ -51664,9 +51705,7 @@ const InputDatetimeLocal = props => {
51664
51705
  const InputWithListResolver = props => {
51665
51706
  const Next = useNextResolver();
51666
51707
  if (props["navi-list"]) {
51667
- return jsx(InputWithList, {
51668
- ...props
51669
- });
51708
+ return renderResolver(InputWithList, props);
51670
51709
  }
51671
51710
  return jsx(Next, {
51672
51711
  ...props
@@ -51773,9 +51812,7 @@ const ChevronDownSvg$1 = () => {
51773
51812
  const InputWithSuggestionsResolver = props => {
51774
51813
  const Next = useNextResolver();
51775
51814
  if (props["navi-suggestions"]) {
51776
- return jsx(InputTextualWithSuggestions, {
51777
- ...props
51778
- });
51815
+ return renderResolver(InputTextualWithSuggestions, props);
51779
51816
  }
51780
51817
  return jsx(Next, {
51781
51818
  ...props
@@ -62527,9 +62564,7 @@ const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
62527
62564
  const PickerCustomResolver = props => {
62528
62565
  import.meta.css = [css$B, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
62529
62566
  if (props.children === undefined) {
62530
- return jsx(PickerNative, {
62531
- ...props
62532
- });
62567
+ return renderResolver(PickerNative, props);
62533
62568
  }
62534
62569
  if (props.mode === "callout") {
62535
62570
  // A tooltip is an icon one presses, unless told otherwise. Own-property
@@ -62591,14 +62626,12 @@ const PickerCustomResolver = props => {
62591
62626
  // how a field says its value is a JS one, kept beside the DOM (see
62592
62627
  // controller_registry.js) — the same thing type="array"/"object" already
62593
62628
  // say for their shapes.
62594
- return jsx(PickerCustom, {
62629
+ return renderResolver(PickerCustom, {
62595
62630
  ...props,
62596
62631
  type: "navi_js"
62597
62632
  });
62598
62633
  }
62599
- return jsx(PickerCustom, {
62600
- ...props
62601
- });
62634
+ return renderResolver(PickerCustom, props);
62602
62635
  };
62603
62636
  const PickerNative = props => {
62604
62637
  const Next = useNextResolver();
@@ -63210,9 +63243,9 @@ const PickerCustom = props => {
63210
63243
  });
63211
63244
  }
63212
63245
  }
63213
- return jsx(PickerContentInsidePopup, {
63246
+ return renderResolver(PickerContentInsidePopup, {
63214
63247
  ...pickerProps,
63215
- mode: mode
63248
+ mode
63216
63249
  });
63217
63250
  };
63218
63251
 
@@ -64479,14 +64512,10 @@ const createItemTracker = (onChange) => {
64479
64512
  const ListItemHeaderOrFooterResolver = props => {
64480
64513
  const Next = useNextResolver();
64481
64514
  if (props.header) {
64482
- return jsx(ListItemHeader, {
64483
- ...props
64484
- });
64515
+ return renderResolver(ListItemHeader, props);
64485
64516
  }
64486
64517
  if (props.footer) {
64487
- return jsx(ListItemFooter, {
64488
- ...props
64489
- });
64518
+ return renderResolver(ListItemFooter, props);
64490
64519
  }
64491
64520
  return jsx(Next, {
64492
64521
  ...props
@@ -64718,9 +64747,7 @@ const ListSelectableContext = createContext(false);
64718
64747
  const ListSelectableResolver = props => {
64719
64748
  const Next = useNextResolver();
64720
64749
  if (props.selectable) {
64721
- return jsx(ListSelectable, {
64722
- ...props
64723
- });
64750
+ return renderResolver(ListSelectable, props);
64724
64751
  }
64725
64752
  return jsx(ListSelectableContext.Provider, {
64726
64753
  value: false,
@@ -65020,7 +65047,7 @@ const ListItemSelectableResolver = props => {
65020
65047
  const isHeaderOrFooter = Boolean(props.header || props.footer);
65021
65048
  const selectable = props.selectable ?? (isHeaderOrFooter || props.role === "presentation" ? false : listSelectable);
65022
65049
  if (selectable) {
65023
- return jsx(ListItemSelectable, {
65050
+ return renderResolver(ListItemSelectable, {
65024
65051
  ...props,
65025
65052
  selectable: true
65026
65053
  });
@@ -70263,14 +70290,10 @@ const generateTimeSlots = (min, max, stepSeconds) => {
70263
70290
  const PickerPresetResolver = props => {
70264
70291
  const Next = useNextResolver();
70265
70292
  if (props.type === "navi_time") {
70266
- return jsx(PickerNaviTime, {
70267
- ...props
70268
- });
70293
+ return renderResolver(PickerNaviTime, props);
70269
70294
  }
70270
70295
  if (props.type === "navi_minute") {
70271
- return jsx(PickerNaviMinute, {
70272
- ...props
70273
- });
70296
+ return renderResolver(PickerNaviMinute, props);
70274
70297
  }
70275
70298
  return jsx(Next, {
70276
70299
  ...props
@@ -71068,59 +71091,37 @@ const PencilSvg = () => {
71068
71091
  const PickerTypeResolver = props => {
71069
71092
  const Next = useNextResolver();
71070
71093
  if (props.type === "color") {
71071
- return jsx(PickerColor, {
71072
- ...props
71073
- });
71094
+ return renderResolver(PickerColor, props);
71074
71095
  }
71075
71096
  if (props.type === "datetime") {
71076
- return jsx(PickerDatetime, {
71077
- ...props
71078
- });
71097
+ return renderResolver(PickerDatetime, props);
71079
71098
  }
71080
71099
  if (props.type === "date") {
71081
- return jsx(PickerDate, {
71082
- ...props
71083
- });
71100
+ return renderResolver(PickerDate, props);
71084
71101
  }
71085
71102
  if (props.type === "month") {
71086
- return jsx(PickerMonth, {
71087
- ...props
71088
- });
71103
+ return renderResolver(PickerMonth, props);
71089
71104
  }
71090
71105
  if (props.type === "week") {
71091
- return jsx(PickerWeek, {
71092
- ...props
71093
- });
71106
+ return renderResolver(PickerWeek, props);
71094
71107
  }
71095
71108
  if (props.type === "time") {
71096
- return jsx(PickerTime, {
71097
- ...props
71098
- });
71109
+ return renderResolver(PickerTime, props);
71099
71110
  }
71100
71111
  if (props.type === "duration") {
71101
- return jsx(PickerDuration, {
71102
- ...props
71103
- });
71112
+ return renderResolver(PickerDuration, props);
71104
71113
  }
71105
71114
  if (props.type === "file") {
71106
- return jsx(PickerFile, {
71107
- ...props
71108
- });
71115
+ return renderResolver(PickerFile, props);
71109
71116
  }
71110
71117
  if (props.type === "text") {
71111
- return jsx(PickerText, {
71112
- ...props
71113
- });
71118
+ return renderResolver(PickerText, props);
71114
71119
  }
71115
71120
  if (props.type === "array") {
71116
- return jsx(PickerArray, {
71117
- ...props
71118
- });
71121
+ return renderResolver(PickerArray, props);
71119
71122
  }
71120
71123
  if (props.type === "object") {
71121
- return jsx(PickerObject, {
71122
- ...props
71123
- });
71124
+ return renderResolver(PickerObject, props);
71124
71125
  }
71125
71126
  return jsx(Next, {
71126
71127
  ...props
@@ -84249,7 +84250,9 @@ const css = /* css */`.navi_side_panel {
84249
84250
  * closes the panel on an outside click instead, and also enables trapping
84250
84251
  * Tab navigation inside the panel (`focusCapture`) — closing on outside
84251
84252
  * interaction only makes sense paired with not letting focus silently
84252
- * leave the panel first.
84253
+ * leave the panel first. A box of the page whose press must not close the
84254
+ * panel (a card that fills it) names the panel:
84255
+ * `data-navi-popup-inside={id}` — see docs/popup_backdrop.md.
84253
84256
  * @param {boolean} [props.swipeToClose=true] - Pushing the panel back
84254
84257
  * towards the edge it is docked to closes it: the panel follows the
84255
84258
  * pointer and finishes leaving (or comes back to rest) when it is
@@ -84384,6 +84387,7 @@ SidePanel.Foot = SidePanelFoot;
84384
84387
 
84385
84388
  const createSlot = (SlotRenderer = Box) => {
84386
84389
  const slotPropsSignal = signal();
84390
+ let filler = null;
84387
84391
  const Slot = () => {
84388
84392
  const props = slotPropsSignal.value;
84389
84393
  return jsx(SlotRenderer, {
@@ -84392,9 +84396,15 @@ const createSlot = (SlotRenderer = Box) => {
84392
84396
  });
84393
84397
  };
84394
84398
  const SlotFill = props => {
84399
+ const fillerRef = useRef();
84400
+ filler = fillerRef;
84395
84401
  slotPropsSignal.value = props;
84396
84402
  useLayoutEffect(() => {
84397
84403
  return () => {
84404
+ if (filler !== fillerRef) {
84405
+ return;
84406
+ }
84407
+ filler = null;
84398
84408
  slotPropsSignal.value = null;
84399
84409
  };
84400
84410
  }, []);