@jsenv/navi 0.29.108 → 0.29.109

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, clickIsSuppressed, watchWheelTravel, scrollRoomTowards, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, initFocusGroup, scrollIntoViewScoped, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
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, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, initFocusGroup, scrollIntoViewScoped, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, batch, untracked, useSignal } from "@preact/signals";
10
10
  import { isValidElement, createContext, render, h, Fragment, toChildArray, options, cloneElement } from "preact";
@@ -25330,75 +25330,85 @@ const setupBrowserIntegrationViaHistory = ({
25330
25330
  return requestedResult;
25331
25331
  };
25332
25332
 
25333
- // Browser event handlers
25333
+ // A click on a link is answered on the link itself, after the link's own
25334
+ // listeners: a navi control decides there whether the press is allowed — it
25335
+ // refuses with preventDefault from its click reaction — and runs its command
25336
+ // and its action; only then does the routing take the browser's place. This
25337
+ // is the order the Navigation API integration gets for free, where the
25338
+ // navigation starts once the click has been dispatched.
25339
+ const onLinkClick = (e) => {
25340
+ if (e.button !== 0) {
25341
+ // Ignore non-left clicks
25342
+ return;
25343
+ }
25344
+ if (e.metaKey) {
25345
+ // Ignore clicks with meta key (e.g. open in new tab)
25346
+ return;
25347
+ }
25348
+ if (e.defaultPrevented) {
25349
+ // Refused — by the link itself, or by whoever came before it.
25350
+ return;
25351
+ }
25352
+ const linkElement = e.currentTarget;
25353
+ if (linkElement.hasAttribute("data-readonly")) {
25354
+ return;
25355
+ }
25356
+ const href = linkElement.href;
25357
+ const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
25358
+ getHrefTargetInfo(href);
25359
+ if (isEmpty || !isSameOrigin) {
25360
+ // Let link to other origins be handled by the browser
25361
+ return;
25362
+ }
25363
+ if (isAnchor) {
25364
+ // Fragment navigation belongs to the browser: it owns the indicated
25365
+ // part of the document, and taking it over would cost `:target` and the
25366
+ // focus handling that come with it.
25367
+ if (isCurrent) {
25368
+ // Except this one, which the browser answers with a scroll and
25369
+ // nothing else: same pathname, same hash, so no event and no url
25370
+ // change reaches whoever is waiting on the designated element.
25371
+ rearmUrlTarget();
25372
+ }
25373
+ return;
25374
+ }
25375
+ // Nothing here declared a route, so there is nothing to route to: the
25376
+ // page is a plain document and a link in it is a plain link. Taking it
25377
+ // over anyway would push the url and then have nothing to show for it —
25378
+ // the address bar moves and the page does not (see applyRouting's own
25379
+ // "not called yet" branch, which is where that used to end up).
25380
+ if (!isRouting()) {
25381
+ return;
25382
+ }
25383
+ e.preventDefault();
25384
+ handleRoutingTask(href, {
25385
+ reason: `"click" on a[href="${href}"]`,
25386
+ // A link that takes the place of the current entry instead of stacking
25387
+ // on it says so on itself (see link_replace.js).
25388
+ navigationType: linkAsksForReplace(linkElement) ? "replace" : "push",
25389
+ // Who started it. Announced with the navigation because a press
25390
+ // carries things the url does not: what a link asks of a route
25391
+ // transition is the first of them (see route_transition.jsx). Read by
25392
+ // whoever knows what to do with it, and it is the anchor itself —
25393
+ // resolved here, where it already is.
25394
+ element: linkElement,
25395
+ });
25396
+ };
25397
+ // Wired from the capture phase on window, the first place a click is seen,
25398
+ // so that a link is answered whatever handler stops the click on its way up
25399
+ // (a card's own onClick). Wired on every click: a listener an element
25400
+ // already has is not added twice, and a link seen for the first time is
25401
+ // wired before the click reaches it. The click a gesture leaves behind
25402
+ // never gets this far — its suppressor (click_suppression.js in
25403
+ // @jsenv/dom) stops it on window, before anything below.
25334
25404
  window.addEventListener(
25335
25405
  "click",
25336
25406
  (e) => {
25337
- if (e.button !== 0) {
25338
- // Ignore non-left clicks
25339
- return;
25340
- }
25341
- if (e.metaKey) {
25342
- // Ignore clicks with meta key (e.g. open in new tab)
25343
- return;
25344
- }
25345
- if (e.defaultPrevented) {
25346
- return;
25347
- }
25348
- if (clickIsSuppressed()) {
25349
- // The click that ends a gesture (click_suppression.js in @jsenv/dom).
25350
- // Its suppressor also listens on window in capture, so whichever
25351
- // module registered first runs first — asked explicitly, the order
25352
- // stops mattering.
25353
- return;
25354
- }
25355
25407
  const linkElement = e.target.closest("a");
25356
25408
  if (!linkElement) {
25357
25409
  return;
25358
25410
  }
25359
- if (linkElement.hasAttribute("data-readonly")) {
25360
- return;
25361
- }
25362
- const href = linkElement.href;
25363
- const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
25364
- getHrefTargetInfo(href);
25365
- if (isEmpty || !isSameOrigin) {
25366
- // Let link to other origins be handled by the browser
25367
- return;
25368
- }
25369
- if (isAnchor) {
25370
- // Fragment navigation belongs to the browser: it owns the indicated
25371
- // part of the document, and taking it over would cost `:target` and the
25372
- // focus handling that come with it.
25373
- if (isCurrent) {
25374
- // Except this one, which the browser answers with a scroll and
25375
- // nothing else: same pathname, same hash, so no event and no url
25376
- // change reaches whoever is waiting on the designated element.
25377
- rearmUrlTarget();
25378
- }
25379
- return;
25380
- }
25381
- // Nothing here declared a route, so there is nothing to route to: the
25382
- // page is a plain document and a link in it is a plain link. Taking it
25383
- // over anyway would push the url and then have nothing to show for it —
25384
- // the address bar moves and the page does not (see applyRouting's own
25385
- // "not called yet" branch, which is where that used to end up).
25386
- if (!isRouting()) {
25387
- return;
25388
- }
25389
- e.preventDefault();
25390
- handleRoutingTask(href, {
25391
- reason: `"click" on a[href="${href}"]`,
25392
- // A link that takes the place of the current entry instead of stacking
25393
- // on it says so on itself (see link_replace.js).
25394
- navigationType: linkAsksForReplace(linkElement) ? "replace" : "push",
25395
- // Who started it. Announced with the navigation because a press
25396
- // carries things the url does not: what a link asks of a route
25397
- // transition is the first of them (see route_transition.jsx). Read by
25398
- // whoever knows what to do with it, and it is the anchor itself —
25399
- // resolved here, where it already is.
25400
- element: linkElement,
25401
- });
25411
+ linkElement.addEventListener("click", onLinkClick);
25402
25412
  },
25403
25413
  { capture: true },
25404
25414
  );
@@ -29838,7 +29848,11 @@ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
29838
29848
  },
29839
29849
  };
29840
29850
  });
29841
- registerNaviCommand("--navi-close", (source, event) => {
29851
+ // "--navi-close:all" closes every expandable above the source, nearest first
29852
+ // a link leaving from a badge shown over a sheet leaves both. A surface that
29853
+ // refuses (a form asking about its changes) keeps what is above it open too:
29854
+ // one cannot be out of the sheet while still in the badge.
29855
+ registerNaviCommand("--navi-close", (source, event, { argument }) => {
29842
29856
  const target =
29843
29857
  resolveExplicitTarget(source) || resolveClosestExpandable(source);
29844
29858
  if (!target) {
@@ -29847,13 +29861,30 @@ registerNaviCommand("--navi-close", (source, event) => {
29847
29861
  return {
29848
29862
  target,
29849
29863
  implementation: () => {
29850
- return dispatchCustomEvent(target, "navi_request_close", {
29851
- event,
29852
- source: resolveCommandProxySource(source),
29853
- });
29864
+ const detail = { event, source: resolveCommandProxySource(source) };
29865
+ if (argument === "all") {
29866
+ return requestCloseUpward(target, detail);
29867
+ }
29868
+ return dispatchCustomEvent(target, "navi_request_close", detail);
29854
29869
  },
29855
29870
  };
29856
29871
  });
29872
+ const requestCloseUpward = (target, detail) => {
29873
+ let expandable = target;
29874
+ while (expandable) {
29875
+ const closing = dispatchCustomEvent(
29876
+ expandable,
29877
+ "navi_request_close",
29878
+ detail,
29879
+ );
29880
+ if (!closing) {
29881
+ return false;
29882
+ }
29883
+ const parent = expandable.parentElement;
29884
+ expandable = parent ? parent.closest("[aria-expanded]") : null;
29885
+ }
29886
+ return true;
29887
+ };
29857
29888
  registerNaviCommand("--navi-cancel", (source, event) => {
29858
29889
  const target =
29859
29890
  resolveExplicitTarget(source) || resolveClosestExpandable(source);
@@ -33573,6 +33604,13 @@ const useControlProps = (props, {
33573
33604
  }
33574
33605
  };
33575
33606
  }
33607
+ if (getKeyboardEventDefaultAction(e) === "activate") {
33608
+ // Enter: the browser presses the link itself, with a click that
33609
+ // follows this keydown — and the click reaction below is where
33610
+ // the press is answered, once. A tab for a slide (no href) gets
33611
+ // no such click; Link answers Enter on its own there.
33612
+ return null;
33613
+ }
33576
33614
  return keyDownDefault(e);
33577
33615
  },
33578
33616
  click: e => {
@@ -39127,9 +39165,12 @@ const LinkPlain = props => {
39127
39165
  replace: undefined,
39128
39166
  "data-navi-route-transition-request": routeTransitionRequest,
39129
39167
  ...replaceRequest,
39168
+ // The control's own handlers first — the interaction gate, the caller's
39169
+ // onClick/onKeyDown, the command and the action — then what only a link
39170
+ // does. Written over the spread above, so they have to be called here.
39130
39171
  onClick: e => {
39131
- onClick?.(e);
39132
- if (slide) {
39172
+ controlHostProps.onClick(e);
39173
+ if (slide && !e.defaultPrevented) {
39133
39174
  goToSlide(e.currentTarget, e);
39134
39175
  }
39135
39176
  if (preventDefault) {
@@ -39142,7 +39183,7 @@ const LinkPlain = props => {
39142
39183
  ,
39143
39184
 
39144
39185
  onKeyDown: e => {
39145
- props.onKeyDown?.(e);
39186
+ controlHostProps.onKeyDown(e);
39146
39187
  if (!slide || e.defaultPrevented) {
39147
39188
  return;
39148
39189
  }
@@ -45772,6 +45813,9 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
45772
45813
  * `--navi-nav-to` command — by TAKING THE PLACE of the current history entry
45773
45814
  * rather than stacking on it: what `<Link replace>` says, for a press drawn
45774
45815
  * as a button.
45816
+ * @param {Function} [action] On a button with an `href` or a `route`, the
45817
+ * same order as a Link's: it runs on the press, before the navigation, and
45818
+ * the navigation does not wait for it (see Link's `action`).
45775
45819
  * @param {boolean} [emojiAsIcon=true] Renders the emoji of the label as icons
45776
45820
  * so the button keeps the height of its text — `Text`'s prop, on by default
45777
45821
  * here. Pass `false` to let an emoji draw at its natural size.
@@ -51781,7 +51825,8 @@ const FOCUS_DELAY_ON_KEYBOARD_MS = 250;
51781
51825
  * - `open()`: requests opening — calls the caller's `onOpen` (see below), then
51782
51826
  * `mountContent`/`openEffect`, then `openHandler`.
51783
51827
  * - `requestClose()`: requests closing — calls `onRequestClose` then `onClose`,
51784
- * stopping after the first if denied. The popup may choose to stay open.
51828
+ * stopping after the first if denied. The popup may choose to stay open,
51829
+ * which is what a `false` return says (`true`: closed, or closed already).
51785
51830
  * - `close()`: closes for real — calls only `onClose`, skipping
51786
51831
  * `onRequestClose` entirely. Used when there really is no choice (e.g. the
51787
51832
  * popup unmounting).
@@ -52074,7 +52119,7 @@ const createOpenController = (
52074
52119
  detail,
52075
52120
  ) => {
52076
52121
  if (!controller.opened) {
52077
- return;
52122
+ return true;
52078
52123
  }
52079
52124
  const requestCloseEvent = new CustomEvent("navi_request_close", {
52080
52125
  detail: { event: e, ...detail },
@@ -52089,9 +52134,10 @@ const createOpenController = (
52089
52134
  if (nativeCancelEvent) {
52090
52135
  nativeCancelEvent.preventDefault();
52091
52136
  }
52092
- return;
52137
+ return false;
52093
52138
  }
52094
52139
  performClose(requestCloseEvent);
52140
+ return true;
52095
52141
  },
52096
52142
  close: (e = new CustomEvent("programmatic", { detail: {} }), detail) => {
52097
52143
  if (!controller.opened) {
@@ -53658,10 +53704,14 @@ const UncontrolledDialog = props => {
53658
53704
  });
53659
53705
  },
53660
53706
  onnavi_request_close: e => {
53661
- openController.requestClose(e, {
53707
+ const closing = openController.requestClose(e, {
53662
53708
  isCancel: e.detail?.isCancel,
53663
53709
  requester: e.detail?.source
53664
53710
  });
53711
+ if (!closing) {
53712
+ // Said back to whoever asked: --navi-close:all stops climbing here.
53713
+ e.preventDefault();
53714
+ }
53665
53715
  }
53666
53716
  });
53667
53717
  };
@@ -55108,9 +55158,13 @@ const UncontrolledPopover = props => {
55108
55158
  });
55109
55159
  },
55110
55160
  onnavi_request_close: e => {
55111
- openController.requestClose(e, {
55161
+ const closing = openController.requestClose(e, {
55112
55162
  isCancel: e.detail?.isCancel
55113
55163
  });
55164
+ if (!closing) {
55165
+ // Said back to whoever asked: --navi-close:all stops climbing here.
55166
+ e.preventDefault();
55167
+ }
55114
55168
  }
55115
55169
  });
55116
55170
  };
@@ -56433,6 +56487,9 @@ const PickerCustomResolver = props => {
56433
56487
  if (circle) {
56434
56488
  props.variant = "icon";
56435
56489
  }
56490
+ // A door, never a field (see `allowNameless`): the form around it expects
56491
+ // no value from it, and no name.
56492
+ props.allowNameless = true;
56436
56493
  // A word in a sentence asks for a plain tooltip — no icon in the callout,
56437
56494
  // no status color; an icon one presses is the callout's own status icon,
56438
56495
  // and says "info" like the callout it opens.
@@ -56782,7 +56839,10 @@ const PickerCustom = props => {
56782
56839
  dispatchCustomEvent(popupRef.current, "navi_request_open", e.detail);
56783
56840
  },
56784
56841
  "onnavi_request_close": e => {
56785
- dispatchCustomEvent(popupRef.current, "navi_request_close", e.detail);
56842
+ const closing = dispatchCustomEvent(popupRef.current, "navi_request_close", e.detail);
56843
+ if (!closing) {
56844
+ e.preventDefault();
56845
+ }
56786
56846
  },
56787
56847
  children
56788
56848
  });
@@ -56834,12 +56894,17 @@ const PickerCustom = props => {
56834
56894
  event: e,
56835
56895
  intent: "read",
56836
56896
  allowed: () => {
56837
- requestClose(e, {
56897
+ const closing = requestClose(e, {
56838
56898
  isCancel: e.detail.isCancel
56839
56899
  });
56900
+ if (!closing) {
56901
+ e.preventDefault();
56902
+ }
56840
56903
  },
56841
56904
  prevented: () => {
56842
56905
  confirmEventRef.current = null;
56906
+ // Not closing either way; said back to whoever asked.
56907
+ e.preventDefault();
56843
56908
  }
56844
56909
  });
56845
56910
  },