@jsenv/navi 0.29.59 → 0.29.60

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, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
6
  export { coarsePointerSignal } from "./jsenv_navi_side_effects.js";
7
- 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, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, 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
+ import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, findEvent, createValueEffect, 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, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
8
8
  export { contrastColor, findEvent, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, batch, untracked, useSignal } from "@preact/signals";
10
10
  import { createContext, isValidElement, h, Fragment, render, toChildArray, options, cloneElement } from "preact";
@@ -7924,6 +7924,9 @@ const css$$ = /* css */`
7924
7924
  * @param {string} [options.status=""] - Callout status: "info" | "warning" | "error" | "success"
7925
7925
  * @param {Function} [options.onClose] - Callback when callout is closed
7926
7926
  * @param {boolean} [options.closeOnClickOutside] - Whether to close on outside clicks (defaults to true for "info" status)
7927
+ * @param {string} [options.reopen="toggle"] - What to do when the anchor already has an open callout:
7928
+ * "toggle" closes it (a second press on what opened it closes it), "update" replaces its message
7929
+ * in place, "replace" tears it down and opens a new one
7927
7930
  * @param {boolean} [options.debug=false] - Enable debug logging
7928
7931
  *
7929
7932
  * Positioning is also driven by attributes read on the anchor element itself
@@ -7964,6 +7967,7 @@ const openCallout = (message, {
7964
7967
  closeOnClickOutside = status === "info",
7965
7968
  closeOnFocusLeave = closeOnClickOutside,
7966
7969
  openingEvent,
7970
+ reopen = "toggle",
7967
7971
  showErrorStack,
7968
7972
  skipFocus = false,
7969
7973
  debug = () => {}
@@ -7972,6 +7976,45 @@ const openCallout = (message, {
7972
7976
  if (debug === true) {
7973
7977
  debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
7974
7978
  }
7979
+ const originalAnchorElement = anchorElement;
7980
+ if (anchorElement) {
7981
+ const proxyElement = findControlProxy(anchorElement);
7982
+ if (proxyElement) {
7983
+ anchorElement = proxyElement;
7984
+ }
7985
+ const controlRoot = findControlRoot(anchorElement);
7986
+ if (controlRoot) {
7987
+ anchorElement = controlRoot;
7988
+ }
7989
+ const anchorVisuallyVisibleInfo = getVisuallyVisibleInfo(anchorElement, {
7990
+ countOffscreenAsVisible: true
7991
+ });
7992
+ if (!anchorVisuallyVisibleInfo.visible) {
7993
+ anchorElement = getFirstVisuallyVisibleAncestor(anchorElement);
7994
+ if (!anchorElement) {
7995
+ // anchorElement is not in the DOM anymore, fallback to body
7996
+ anchorElement = document.body;
7997
+ }
7998
+ console.warn(`anchor is not visually visible (${anchorVisuallyVisibleInfo.reason}) -> callout will anchor to first visually visible ancestor (${getElementSignature(anchorElement)})`);
7999
+ }
8000
+ }
8001
+ if (anchorElement) {
8002
+ const calloutAlreadyThere = anchorElement.callout;
8003
+ if (calloutAlreadyThere && calloutAlreadyThere.opened) {
8004
+ if (reopen === "toggle") {
8005
+ debug(openingEvent, `callout already open on ${getElementSignature(anchorElement)} -> close it (reopen: "toggle")`);
8006
+ calloutAlreadyThere.requestClose(openingEvent, "reopen_toggle");
8007
+ return calloutAlreadyThere;
8008
+ }
8009
+ if (reopen === "update") {
8010
+ debug(openingEvent, `callout already open on ${getElementSignature(anchorElement)} -> update it (reopen: "update")`);
8011
+ calloutAlreadyThere.update(message, {
8012
+ status
8013
+ });
8014
+ return calloutAlreadyThere;
8015
+ }
8016
+ }
8017
+ }
7975
8018
  const callout = {
7976
8019
  opened: true,
7977
8020
  close: null,
@@ -8155,28 +8198,7 @@ const openCallout = (message, {
8155
8198
  callout.updatePosition();
8156
8199
  }
8157
8200
  };
8158
- const originalAnchorElement = anchorElement;
8159
- if (anchorElement) {
8160
- const proxyElement = findControlProxy(anchorElement);
8161
- if (proxyElement) {
8162
- anchorElement = proxyElement;
8163
- }
8164
- const controlRoot = findControlRoot(anchorElement);
8165
- if (controlRoot) {
8166
- anchorElement = controlRoot;
8167
- }
8168
- const anchorVisuallyVisibleInfo = getVisuallyVisibleInfo(anchorElement, {
8169
- countOffscreenAsVisible: true
8170
- });
8171
- if (!anchorVisuallyVisibleInfo.visible) {
8172
- anchorElement = getFirstVisuallyVisibleAncestor(anchorElement);
8173
- if (!anchorElement) {
8174
- // anchorElement is not in the DOM anymore, fallback to body
8175
- anchorElement = document.body;
8176
- }
8177
- console.warn(`anchor is not visually visible (${anchorVisuallyVisibleInfo.reason}) -> callout will anchor to first visually visible ancestor (${getElementSignature(anchorElement)})`);
8178
- }
8179
- }
8201
+
8180
8202
  // Resolve the visual anchor for positioning: when data-callout-anchor is set,
8181
8203
  // use the inner element it points to. anchorElement remains the container
8182
8204
  // that receives data-callout and CSS vars.
@@ -8212,6 +8234,14 @@ const openCallout = (message, {
8212
8234
  return anchorElement.parentNode || document.body;
8213
8235
  })();
8214
8236
  {
8237
+ // document.body as anchor means "no anchor" (the callout is docked in the
8238
+ // viewport); everything would be inside it.
8239
+ const isInsideAnchor = target => {
8240
+ if (!anchorElement || anchorElement === document.body) {
8241
+ return false;
8242
+ }
8243
+ return anchorElement === target || anchorElement.contains(target);
8244
+ };
8215
8245
  const handleClickOutside = event => {
8216
8246
  if (event.button !== 0) {
8217
8247
  // right click
@@ -8221,6 +8251,16 @@ const openCallout = (message, {
8221
8251
  if (clickTarget === calloutElement || calloutElement.contains(clickTarget)) {
8222
8252
  return;
8223
8253
  }
8254
+ if (isInsideAnchor(clickTarget)) {
8255
+ // Pressing the anchor is not "outside": this listener is on document in
8256
+ // the capture phase, so closing here would destroy the callout before
8257
+ // the event reaches the handler that owns it — and that handler,
8258
+ // opening a callout on the very anchor it was just removed from, would
8259
+ // create a second one within the same click. Left open, openCallout's
8260
+ // `reopen` decides (toggle by default).
8261
+ debug(event, `click on anchor, let the anchor handler decide`);
8262
+ return;
8263
+ }
8224
8264
  requestClose(event, "click_outside");
8225
8265
  };
8226
8266
  const handleSpaceOutside = event => {
@@ -8231,6 +8271,12 @@ const openCallout = (message, {
8231
8271
  if (keyTarget === calloutElement || calloutElement.contains(keyTarget)) {
8232
8272
  return;
8233
8273
  }
8274
+ if (isInsideAnchor(keyTarget)) {
8275
+ // Space on the anchor produces a click afterwards — same reasoning as
8276
+ // handleClickOutside above.
8277
+ debug(event, `space on anchor, let the anchor handler decide`);
8278
+ return;
8279
+ }
8234
8280
  requestClose(event, "space_outside");
8235
8281
  };
8236
8282
  const registerClickOutsideListener = () => {
@@ -8403,7 +8449,9 @@ const openCallout = (message, {
8403
8449
  });
8404
8450
  allowWheelThrough(calloutElement, visualAnchorElement);
8405
8451
  anchorElement.setAttribute("data-callout", calloutId);
8406
- addTeardown(() => {
8452
+ addTeardown(({
8453
+ event
8454
+ }) => {
8407
8455
  anchorElement.removeAttribute("data-callout");
8408
8456
  });
8409
8457
  const visualElement = (() => {
@@ -29086,12 +29134,13 @@ const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
29086
29134
  /**
29087
29135
  * Builds the `pointerdown` handler a popup docked to `side` answers with.
29088
29136
  *
29089
- * `grip` narrows where the gesture may start to one part of the popup, given as
29090
- * a selector matched inside it: a popup whose whole surface is a place to push
29091
- * from has nothing else to do with the press, while one made of content the
29092
- * finger operates (a bottom sheet full of controls) only offers the strip that
29093
- * is there to be held. A popup that has no such part is pushed from anywhere,
29094
- * so naming a grip never leaves it undismissable.
29137
+ * `grip` says where the gesture may start, as a selector the pressed element is
29138
+ * matched against with `closest`. A popup that names one is held THERE and
29139
+ * nowhere else: everything else it holds is content the finger came to operate,
29140
+ * and a press on it is that content's including gestures of its own, which
29141
+ * this file has no way of knowing about. A popup that names no grip is pushed
29142
+ * from its whole surface, which only suits one made of nothing else (a side
29143
+ * panel showing a page).
29095
29144
  */
29096
29145
  const createSwipeToClose = (side, { grip } = {}) => {
29097
29146
  const axis = SWIPE_AXIS_BY_SIDE[side];
@@ -29103,8 +29152,11 @@ const createSwipeToClose = (side, { grip } = {}) => {
29103
29152
  return;
29104
29153
  }
29105
29154
  if (grip) {
29106
- const gripEl = panelEl.querySelector(grip);
29107
- if (gripEl && !gripEl.contains(pointerDownEvent.target)) {
29155
+ // Read from the press outwards rather than by looking the grip up in the
29156
+ // panel: a popup may hold several (a header and whatever else was marked
29157
+ // as one), and where they sit in it is the application's business.
29158
+ const gripEl = pointerDownEvent.target.closest(grip);
29159
+ if (!gripEl || !panelEl.contains(gripEl)) {
29108
29160
  return;
29109
29161
  }
29110
29162
  }
@@ -29492,13 +29544,14 @@ const css$W = /* css */`
29492
29544
  &:has(> [data-focus-outline-delegate][data-focus-visible]) {
29493
29545
  outline-style: solid;
29494
29546
  }
29495
- /* The header of a sheet that closes by being pushed back down is a handle,
29496
- not a piece of the scroll: a finger on it moves the sheet, and letting
29497
- the browser scroll the sheet under the same gesture would show two
29498
- movements answering one drag. Direct children only the sheet's own
29499
- header, not one belonging to something it contains (the same part
29500
- swipe_to_close.js takes hold of). */
29501
- &[data-swipe-to-close] > [data-header] {
29547
+ /* What a sheet that closes by being pushed back down is held by is a
29548
+ handle, not a piece of the scroll: a finger on it moves the sheet, and
29549
+ letting the browser scroll the sheet under the same gesture would show
29550
+ two movements answering one drag. The same parts swipe_to_close.js takes
29551
+ hold of, said again here because CSS is the only place it can be said
29552
+ before the finger lands. */
29553
+ &[data-swipe-to-close] [data-header],
29554
+ &[data-swipe-to-close] [data-swipe-grip] {
29502
29555
  touch-action: none;
29503
29556
  }
29504
29557
 
@@ -29647,8 +29700,10 @@ const css$W = /* css */`
29647
29700
  * edge docking would bring it to, so docking could only take away the shape
29648
29701
  * the caller asked for. Re-resolves live as the pointer
29649
29702
  * type or the window size changes. A sheet resting on the bottom edge is also
29650
- * pushed back down to close it, held by its header (a direct child `Box` with
29651
- * the `header` prop) or from anywhere when it has none. See `swipe_to_close.js`.
29703
+ * pushed back down to close it, held by its header (a `Box` with the `header`
29704
+ * prop) and by anything else carrying `data-swipe-grip`. The rest of the sheet
29705
+ * is left to what it holds, so a board something is dragged across keeps its
29706
+ * own gestures. See `swipe_to_close.js`.
29652
29707
  * @param {string} [props.positionArea="center"] - Where to dock the dialog
29653
29708
  * within its container (the viewport for `layer="top"`, the positioned
29654
29709
  * ancestor for `layer="local"`) — Dialog is never anchored to a real
@@ -29902,12 +29957,13 @@ const DOCKED = {
29902
29957
  scrollCapture: true
29903
29958
  };
29904
29959
 
29905
- // Where a bottom sheet is held to push it back down: the strip a direct-child
29906
- // Box declares with `header` the rest of the sheet is content the finger
29907
- // operates, and a drag started there would fight whatever it landed on. A sheet
29908
- // with no header of its own is pushed from anywhere (see createSwipeToClose's
29909
- // own `grip`).
29910
- const DOCKED_SWIPE_GRIP = ":scope > [data-header]";
29960
+ // Where a bottom sheet is held to push it back down: the strip a Box declares
29961
+ // with `header`, plus anything the application marked as one more. Everything
29962
+ // else in the sheet is content the finger came to operate a board a piece is
29963
+ // dragged across, a list, a map and a press there belongs to it. A sheet with
29964
+ // no header and nothing marked is not pushed down at all; it is closed by its
29965
+ // own controls, by the backdrop and by Escape.
29966
+ const DOCKED_SWIPE_GRIP = "[data-header],[data-swipe-grip]";
29911
29967
 
29912
29968
  // The first control inside `dialogEl` that is mid-action, if any. Walks the
29913
29969
  // controls rather than reading an attribute off the dialog: a dialog carries no