@jsenv/navi 0.29.464 → 0.29.465

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  * using @jsenv/navi as intended.
4
4
  */
5
5
  import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
- import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, ELEMENT_SIZE_CHANGE, getScrollContainer, scrollIntoViewScoped, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, trapScrollInside, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange, getVirtualKeyboardOverlayHeight } from "@jsenv/dom";
6
+ import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, getScrollContainer, watchWheelTravel, scrollRoomTowards, trapScrollInside, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange, getVirtualKeyboardOverlayHeight } from "@jsenv/dom";
7
7
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
8
8
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
9
9
  import { options, isValidElement, createContext, render as render$1, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
@@ -79,7 +79,7 @@ installImportMetaCssBuild(import.meta);
79
79
  * any of these, and a number is the last resort, not the first tool.
80
80
  */
81
81
 
82
- const css$18 = /* css */ `@layer navi {
82
+ const css$19 = /* css */ `@layer navi {
83
83
  :root {
84
84
  --navi-z-index-control-hovered: 1;
85
85
  --navi-z-index-control-focused: 2;
@@ -92,7 +92,7 @@ const css$18 = /* css */ `@layer navi {
92
92
  }
93
93
  }
94
94
  `;
95
- import.meta.css = [css$18, "@jsenv/navi/src/navi_z_indexes.js"];
95
+ import.meta.css = [css$19, "@jsenv/navi/src/navi_z_indexes.js"];
96
96
 
97
97
  /**
98
98
  * A navigation is ABOUT to be applied — said before its very first write.
@@ -446,7 +446,7 @@ installImportMetaCssBuild(import.meta);
446
446
 
447
447
  const URL_TARGET_ATTRIBUTE = "data-url-target";
448
448
 
449
- const css$17 = /* css */ `@layer navi {
449
+ const css$18 = /* css */ `@layer navi {
450
450
  [data-url-target] {
451
451
  animation: navi_url_target var(--navi-url-target-duration, 2s)
452
452
  ease-out;
@@ -464,7 +464,7 @@ const css$17 = /* css */ `@layer navi {
464
464
  }
465
465
  }
466
466
  `;
467
- import.meta.css = [css$17, "@jsenv/navi/src/nav/url_target/url_target.js"];
467
+ import.meta.css = [css$18, "@jsenv/navi/src/nav/url_target/url_target.js"];
468
468
 
469
469
  let urlTargetOptions = {
470
470
  block: "start",
@@ -2105,7 +2105,7 @@ const useActionStatus = (action) => {
2105
2105
  };
2106
2106
 
2107
2107
  installImportMetaCssBuild(import.meta);
2108
- const css$16 = /* css */`.action_error {
2108
+ const css$17 = /* css */`.action_error {
2109
2109
  background: #fdd;
2110
2110
  border: 1px solid red;
2111
2111
  margin-top: 0;
@@ -2129,7 +2129,7 @@ const ActionRenderer = ({
2129
2129
  children,
2130
2130
  disabled
2131
2131
  }) => {
2132
- import.meta.css = [css$16, "@jsenv/navi/src/action/action_renderer.jsx"];
2132
+ import.meta.css = [css$17, "@jsenv/navi/src/action/action_renderer.jsx"];
2133
2133
  if (action === undefined) {
2134
2134
  throw new Error("ActionRenderer requires an action to render, but none was provided.");
2135
2135
  }
@@ -15838,6 +15838,48 @@ const createOpenController = (
15838
15838
  // armSuppressNextOpenRequest below.
15839
15839
  let suppressNextOpenRequest = false;
15840
15840
  let disarmSuppressNextOpenRequest = null;
15841
+ // The press a press-close came from, for as long as that press lasts (see
15842
+ // open() for what it is read for).
15843
+ let pressCountAtPressClose = null;
15844
+ let releaseThisPress = null;
15845
+ const holdReopenForThisPress = (closeEvent) => {
15846
+ releaseThisPress?.();
15847
+ pressCountAtPressClose = pressCount;
15848
+ let timeout = null;
15849
+ const release = () => {
15850
+ clearTimeout(timeout);
15851
+ document.removeEventListener("click", onPressEnd, { capture: true });
15852
+ document.removeEventListener("pointercancel", onPressEnd, {
15853
+ capture: true,
15854
+ });
15855
+ document.removeEventListener("pointerdown", release, { capture: true });
15856
+ pressCountAtPressClose = null;
15857
+ releaseThisPress = null;
15858
+ };
15859
+ // Once the task the press ends in is over: every reaction to the closing
15860
+ // press is synchronous to it, while an open asked for later — a command a
15861
+ // button runs once its action resolves — is somebody asking again.
15862
+ const onPressEnd = () => {
15863
+ document.removeEventListener("click", onPressEnd, { capture: true });
15864
+ document.removeEventListener("pointercancel", onPressEnd, {
15865
+ capture: true,
15866
+ });
15867
+ timeout = setTimeout(release);
15868
+ };
15869
+ if (findEvent(closeEvent, "click")) {
15870
+ // Closed by the press's own click: a listener added during its dispatch
15871
+ // would only hear the next one.
15872
+ timeout = setTimeout(release);
15873
+ } else {
15874
+ document.addEventListener("click", onPressEnd, { capture: true });
15875
+ document.addEventListener("pointercancel", onPressEnd, {
15876
+ capture: true,
15877
+ });
15878
+ }
15879
+ // A press whose click was swallowed (a gesture) ends with the next one.
15880
+ document.addEventListener("pointerdown", release, { capture: true });
15881
+ releaseThisPress = release;
15882
+ };
15841
15883
 
15842
15884
  // When the popup closes because of a press (clicking the backdrop, or a
15843
15885
  // press outside a popup with no backdrop), the browser still dispatches the
@@ -15993,6 +16035,7 @@ const createOpenController = (
15993
16035
  `closed by ${pressEvent.type} -> ignore next click`,
15994
16036
  );
15995
16037
  armSuppressNextOpenRequest();
16038
+ holdReopenForThisPress(closeEvent);
15996
16039
  break prevent_reopen;
15997
16040
  }
15998
16041
 
@@ -16127,7 +16170,9 @@ const createOpenController = (
16127
16170
  // called once the open/close has fully happened rather than mid-sequence.
16128
16171
  onOpenedChange: null,
16129
16172
  open: (e, detail) => {
16130
- flushChangeAwaitingTransition();
16173
+ // The refusals below read the decision (`opened`), which is written
16174
+ // before a change waiting for its view transition runs: a refused open
16175
+ // must not force that change through and take its picture away.
16131
16176
  if (controller.opened || !controller.openEffect) {
16132
16177
  return;
16133
16178
  }
@@ -16135,6 +16180,20 @@ const createOpenController = (
16135
16180
  suppressNextOpenRequest = false;
16136
16181
  return;
16137
16182
  }
16183
+ if (
16184
+ pressCountAtPressClose === pressCount &&
16185
+ findEvent(e, isEventOfAPress)
16186
+ ) {
16187
+ // The press that closed it, heard again by what opens it. With nothing
16188
+ // between a popup and its own trigger (no wall, or a PopupGroup), the
16189
+ // press is still on its way when the popup closes, and the trigger's
16190
+ // own reaction to that same press asks to open it again: at its
16191
+ // mousedown, before the click the suppression above waits for — or at
16192
+ // the very click that closed it, whose suppression is armed during its
16193
+ // own dispatch and so only hears the next one.
16194
+ return;
16195
+ }
16196
+ flushChangeAwaitingTransition();
16138
16197
  const requestOpenEvent = new CustomEvent("navi_request_open", {
16139
16198
  detail: { event: e, ...detail },
16140
16199
  cancelable: true,
@@ -16266,6 +16325,18 @@ const createOpenController = (
16266
16325
  });
16267
16326
  chainEvent(requestCloseEvent, e);
16268
16327
  closeHandlers?.onRequestClose?.(requestCloseEvent);
16328
+ // An Escape this popup answered, by closing or by refusing: dispatched
16329
+ // on, it would also be the browser's close request for a modal around
16330
+ // the popup (a PopupGroup, a dialog), closing what this popup kept open,
16331
+ // or what is still closing with it (a lifting dialog closes a frame
16332
+ // later).
16333
+ const escapeKeydown = findEvent(
16334
+ requestCloseEvent,
16335
+ (event) => event.type === "keydown" && event.key === "Escape",
16336
+ );
16337
+ if (escapeKeydown) {
16338
+ escapeKeydown.preventDefault();
16339
+ }
16269
16340
  if (requestCloseEvent.defaultPrevented) {
16270
16341
  // The native <dialog> "cancel" event (Escape key) closes the dialog
16271
16342
  // by default; prevent that default so denial actually keeps it open.
@@ -16294,13 +16365,24 @@ const createOpenController = (
16294
16365
  return controller;
16295
16366
  };
16296
16367
 
16368
+ // A click a keyboard made (detail 0) is not one: nothing pressed for it.
16369
+ const isEventOfAPress = (event) =>
16370
+ event.type === "pointerdown" ||
16371
+ event.type === "mousedown" ||
16372
+ (event.type === "click" && event.detail > 0);
16373
+
16297
16374
  // Inside a popup that is open right now — the popup being closed, in practice,
16298
16375
  // since that is the one the key press was delivered to.
16299
16376
  const isInsideOpenPopup = (element) => {
16300
16377
  if (!element || element.nodeType !== 1) {
16301
16378
  return false;
16302
16379
  }
16303
- return Boolean(element.closest("dialog[open], [popover]:popover-open"));
16380
+ // An engaged PopupGroup is open too, around peers that are not the popup.
16381
+ return Boolean(
16382
+ element.closest(
16383
+ "dialog[open]:not([navi-popup-group]), [popover]:popover-open",
16384
+ ),
16385
+ );
16304
16386
  };
16305
16387
 
16306
16388
  // What Enter is about to do when it is about to activate something: press the
@@ -16766,572 +16848,6 @@ const useOpenControllerByProps = (props, name) => {
16766
16848
  return openController;
16767
16849
  };
16768
16850
 
16769
- /**
16770
- * Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
16771
- * (non-top-layer) renderers — operate on a plain DOM element, no knowledge
16772
- * of which of the two owns it.
16773
- */
16774
-
16775
-
16776
- /**
16777
- * Whether a visibleRectEffect delivery is one that can have taken height away
16778
- * from a popup, and so pushed whatever holds focus out of sight:
16779
- * - "resize": the window/visual viewport settled — which is also how the
16780
- * on-screen keyboard arrives, overlay or not (window_size.js);
16781
- * - ELEMENT_SIZE_CHANGE: the popup's own box measured different;
16782
- * - "focusin": the focus-settled re-measure, for the room that changes with
16783
- * nothing announcing it (subscribeFocusSettled in window_size.js).
16784
- *
16785
- * Everything else is a scroll of one kind or another, where nothing got
16786
- * smaller and scrolling the focused element back would fight the very gesture
16787
- * that fired it.
16788
- */
16789
- const mayHaveHiddenFocus = (event) => {
16790
- const type = event?.type;
16791
- return (
16792
- type === "resize" || type === ELEMENT_SIZE_CHANGE || type === "focusin"
16793
- );
16794
- };
16795
-
16796
- const scrollportHeightMap = new WeakMap();
16797
- /**
16798
- * Scrolls whatever holds focus inside `popupEl` back into view, if the popup
16799
- * getting shorter has pushed it out.
16800
- *
16801
- * The case this exists for: a field low in the scrolling body of a popup that
16802
- * also has a footer (box.jsx — with a body, the body is the only thing that
16803
- * scrolls and the footer is a sibling sitting right under it). Focusing the
16804
- * field makes the browser scroll it into view, which it does against the
16805
- * popup's height AT THAT MOMENT; the on-screen keyboard then opens and takes
16806
- * that height away. The body shrinks, its scrollTop does not move, so the
16807
- * content slides down relative to the shorter scrollport and the field ends up
16808
- * past its bottom edge — visually, swallowed by the footer. The browser does
16809
- * not redo a scroll-into-view it already answered, so this does.
16810
- *
16811
- * Only what the shrink itself hid, though: with the keyboard up the user reads
16812
- * the rest of the popup by scrolling the field away — to reach the submit under
16813
- * it, typically — and the room keeps changing while they do (a keyboard settling
16814
- * in two steps, a suggestion strip, a browser bar). Answering each of those by
16815
- * scrolling the field back takes the popup away from wherever they had just
16816
- * scrolled it, over and over: what they were reading cannot be reached at all
16817
- * without blurring the field first, and the popup reads as unscrollable. So the
16818
- * field is brought back only when it was in view before the room shrank, which
16819
- * one remembered number answers: a resize moves neither scrollTop nor the
16820
- * element's offset inside the scrolled content, so measuring against the height
16821
- * the scrollport HAD is measuring the state before the change.
16822
- *
16823
- * Scoped to the field's own scroll container (never the page): a popup traps
16824
- * scrolling precisely so the document underneath cannot move, and a plain
16825
- * scrollIntoView walks past a container whose scrollbar isn't visible — see
16826
- * scrollIntoViewScoped's own doc.
16827
- *
16828
- * "nearest": the smallest scroll that makes it visible, and none at all when it
16829
- * already is. Where it lands is the container's own business — a navi scroller
16830
- * keeps a band free at its edges so a field never comes back glued to one (see
16831
- * scroll-padding in box.jsx).
16832
- */
16833
- const keepFocusedElementVisible = (popupEl) => {
16834
- const { activeElement } = document;
16835
- if (!activeElement || activeElement === popupEl) {
16836
- return;
16837
- }
16838
- if (!popupEl.contains(activeElement)) {
16839
- return;
16840
- }
16841
- const scrollContainer = getScrollContainer(activeElement);
16842
- if (!scrollContainer || !popupEl.contains(scrollContainer)) {
16843
- // What scrolls the field is outside the popup, which means the page: a
16844
- // popup holds it still on purpose (trapScrollInside), so there is nothing
16845
- // here to scroll back.
16846
- return;
16847
- }
16848
- const scrollportHeight = scrollContainer.clientHeight;
16849
- const scrollportHeightBefore = scrollportHeightMap.get(scrollContainer);
16850
- scrollportHeightMap.set(scrollContainer, scrollportHeight);
16851
- if (scrollportHeightBefore !== undefined) {
16852
- if (scrollportHeight >= scrollportHeightBefore) {
16853
- // Nothing was taken away, so nothing was hidden by this.
16854
- return;
16855
- }
16856
- if (
16857
- !isVisibleInScrollport(
16858
- activeElement,
16859
- scrollContainer,
16860
- scrollportHeightBefore,
16861
- )
16862
- ) {
16863
- return;
16864
- }
16865
- }
16866
- scrollIntoViewScoped(activeElement, {
16867
- container: scrollContainer,
16868
- block: "nearest",
16869
- });
16870
- };
16871
-
16872
- // Whether any part of `el` was showing in `container` back when its scrollport
16873
- // was `scrollportHeight` tall. Both boxes are read now: the container may have
16874
- // moved as well as shrunk (a centered dialog re-centers itself), and the
16875
- // difference between the two tops is what that move leaves alone.
16876
- const isVisibleInScrollport = (el, container, scrollportHeight) => {
16877
- const elRect = el.getBoundingClientRect();
16878
- const containerRect = container.getBoundingClientRect();
16879
- const elTop = elRect.top - containerRect.top + container.scrollTop;
16880
- const elBottom = elTop + elRect.height;
16881
- const scrollportTop = container.scrollTop;
16882
- const scrollportBottom = scrollportTop + scrollportHeight;
16883
- return elBottom > scrollportTop && elTop < scrollportBottom;
16884
- };
16885
-
16886
- /**
16887
- * Calls `onSettled` once `el`'s current CSS transition is over — via
16888
- * `transitionend`, with a safety `setTimeout` fallback matching the longest
16889
- * `transition-duration`, in case nothing actually transitions or an event is
16890
- * missed.
16891
- *
16892
- * Returns a "cancel" function, so a caller whose instance has been superseded
16893
- * (a fresh open/close about to set its own state) can keep this stale one from
16894
- * firing later. Cancelling only stops `onSettled`: undoing whatever the caller
16895
- * did up front is that fresh call's business, not this one's.
16896
- */
16897
- const whenTransitionSettles = (el, onSettled) => {
16898
- let settled = false;
16899
- const onTransitionEnd = (transitionEvent) => {
16900
- if (transitionEvent.target === el) {
16901
- finish();
16902
- }
16903
- };
16904
- const stopWatching = () => {
16905
- settled = true;
16906
- el.removeEventListener("transitionend", onTransitionEnd);
16907
- clearTimeout(safetyTimeoutId);
16908
- };
16909
- const finish = () => {
16910
- if (settled) {
16911
- return;
16912
- }
16913
- stopWatching();
16914
- onSettled();
16915
- };
16916
- el.addEventListener("transitionend", onTransitionEnd);
16917
- const durationsInSeconds = getComputedStyle(el)
16918
- .transitionDuration.split(",")
16919
- .map((value) => parseFloat(value) || 0);
16920
- const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
16921
- const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
16922
- return () => {
16923
- if (settled) {
16924
- return;
16925
- }
16926
- stopWatching();
16927
- };
16928
- };
16929
-
16930
- /**
16931
- * Drops the document's text selection when it lives inside `el`, leaving a
16932
- * selection made elsewhere on the page alone.
16933
- *
16934
- * A popup being closed takes its content with it, and a selection is a claim
16935
- * on content the user can still act on: kept, it would outlive the surface it
16936
- * was made on — painted on the box for the length of its exit transition, with
16937
- * the handles and the copy toolbar a phone draws from the live selection
16938
- * hanging over something that is going away — and reappear with the box on
16939
- * the next opening. Dropping the selection is the only way to remove that
16940
- * chrome: a `user-select: none` on the closing box hides the highlight in some
16941
- * browsers only, and the handles are the browser's own, drawn from the
16942
- * selection object rather than from any style.
16943
- */
16944
- const clearTextSelectionInside = (el) => {
16945
- const selection = window.getSelection();
16946
- if (!selection || selection.rangeCount === 0) {
16947
- return;
16948
- }
16949
- const range = selection.getRangeAt(0);
16950
- if (!el.contains(range.commonAncestorContainer)) {
16951
- return;
16952
- }
16953
- selection.removeAllRanges();
16954
- };
16955
-
16956
- /**
16957
- * Disables pointer-events on `el` until its current CSS transition settles —
16958
- * avoids the cursor changing/something becoming clickable while the popup is
16959
- * still visually moving into or out of place.
16960
- *
16961
- * Returns whenTransitionSettles' own "cancel" function: it doesn't restore
16962
- * pointer-events, since a fresh call for the next open/close is about to set
16963
- * its own state.
16964
- */
16965
- const suppressPointerEventsDuringTransition = (el) => {
16966
- el.style.pointerEvents = "none";
16967
- return whenTransitionSettles(el, () => {
16968
- el.style.pointerEvents = "";
16969
- });
16970
- };
16971
-
16972
- /**
16973
- * Hides the backdrop, deferring until the browser's matching "click" fires
16974
- * when `closeEvent` was triggered by a mousedown (see popover.jsx's top
16975
- * comment for why) — same capture-phase-on-document pattern as
16976
- * armSuppressNextOpenRequest in open_controller.js, which a plain timeout
16977
- * can't safely replace: mouseup (and the click that follows it) can land an
16978
- * arbitrarily long time after mousedown (the user is still holding the
16979
- * button down), so a short timeout can fire first and hide the backdrop
16980
- * before its own click ever arrives. A capture-phase listener on document
16981
- * fires for every click regardless of what any bubble-phase handler does
16982
- * downstream, so no fallback timer is needed.
16983
- *
16984
- * `hide` is the caller's own way to actually hide the backdrop
16985
- * (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
16986
- * for a plain div) — this helper only owns the mousedown/click timing.
16987
- *
16988
- * Returns a disarm function (or undefined if hidden immediately), so a
16989
- * fresh open can cancel a pending hide it's about to make redundant.
16990
- */
16991
- const armPointerDownOutsideClose = (closeEvent, hide) => {
16992
- const mousedownEvent = findEvent(closeEvent, "mousedown");
16993
- if (!mousedownEvent) {
16994
- hide();
16995
- return undefined;
16996
- }
16997
- // `hide` is handed the click, for a wall with peers to re-aim it (see
16998
- // handWallClickToPeer).
16999
- const onClick = (clickEvent) => {
17000
- document.removeEventListener("click", onClick, { capture: true });
17001
- hide(clickEvent);
17002
- };
17003
- document.addEventListener("click", onClick, { capture: true });
17004
- return () => {
17005
- document.removeEventListener("click", onClick, { capture: true });
17006
- };
17007
- };
17008
-
17009
- /**
17010
- * Maps a positionArea y/x pair to a concrete `navi-animation` value (a
17011
- * `prefix` plus a direction word), or `null` if both axes overlap the anchor
17012
- * (no direction at all — that's `resolvedAnimationKind === "scaling"`
17013
- * territory instead, see resolveAutoAnimationKind below).
17014
- *
17015
- * `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
17016
- * when docked) keeps the word as the compass direction the popup comes
17017
- * from: placed "top" (a point/corner), it slides in from the top.
17018
- * `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
17019
- * direction instead, the opposite compass point: placed "top" of the
17020
- * anchor, it moves/grows up, away from the anchor (which sits below it).
17021
- *
17022
- * "inset-*"/"center" contribute no direction on their axis either way.
17023
- */
17024
- const resolveDirectionValue = (y, x, { prefix }) => {
17025
- const yWord =
17026
- y === "top"
17027
- ? prefix === "expand"
17028
- ? "up"
17029
- : "top"
17030
- : y === "bottom"
17031
- ? prefix === "expand"
17032
- ? "down"
17033
- : "bottom"
17034
- : null;
17035
- const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
17036
- if (!yWord && !xWord) {
17037
- return null;
17038
- }
17039
- return yWord && xWord
17040
- ? `${prefix}-${yWord}-${xWord}`
17041
- : `${prefix}-${yWord || xWord}`;
17042
- };
17043
-
17044
- /**
17045
- * Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
17046
- * — picked for any real anchor, or for a point/corner placed dead-center
17047
- * (both positionArea axes overlapping — there's no sensible direction to
17048
- * slide from in that case). "sliding" otherwise. `anchor` is `undefined`
17049
- * for any no-anchor/docked case (Dialog always, Popover's own custom
17050
- * renderer when there's no real anchor), so this collapses to "scaling"
17051
- * there only for the dead-center case, "sliding" otherwise. The two
17052
- * "overlapping" booleans below describe the *positionArea* itself (a bare
17053
- * word vs. "inset-"/"center"), not anything about the anchor — they'd
17054
- * mean exactly the same thing even with no anchor at all, since it's the
17055
- * position strategy, not the anchor, that decides whether there's a
17056
- * direction to slide from.
17057
- */
17058
- const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
17059
- const yIsOverlapping =
17060
- parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
17061
- const xIsOverlapping =
17062
- parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
17063
- return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
17064
- };
17065
-
17066
- /**
17067
- * A popup reads "outside" from its own border box: what a press lands on
17068
- * decides nothing on its own (a genuine backdrop press and a press on the
17069
- * popup's padding both report the popup element as their target, there being
17070
- * no real ::backdrop node to be one), so the rectangle is what tells them
17071
- * apart. That holds as long as the box and what the popup paints are the same
17072
- * thing — which stops being true for a popup with no surface of its own
17073
- * (`backgroundColor="transparent"`, no shadow, no padding): what the eye reads
17074
- * as backdrop is then inside the box, and a press there stays a press on the
17075
- * popup.
17076
- *
17077
- * `data-navi-popup-outside` is how a caller says which of its own boxes are
17078
- * not the surface. It is opt-in because navi cannot infer it — a background
17079
- * can come from anywhere — while the caller who made the popup see-through
17080
- * knows exactly which box is decoration and which is paper.
17081
- *
17082
- * The marker answers for the element it is on, never for its descendants: a
17083
- * box painted inside a marked one is still surface, so a row can be marked and
17084
- * its empty halves read as backdrop while presses on the controls it holds do
17085
- * not dismiss anything. A descendant with `pointer-events: none` never becomes
17086
- * a press target at all, so the marked box answers in its place — while the
17087
- * same declaration on the marked box itself takes it out of hit-testing and
17088
- * makes the marker unreachable (see warnAboutUnreachableOutsideRegions).
17089
- */
17090
- const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
17091
-
17092
- /**
17093
- * `data-navi-popup-inside` is the other direction: a box of the PAGE the
17094
- * caller says belongs to a popup, so a press on it is not outside that popup
17095
- * even though it lies beyond the border box — a card whose press is what
17096
- * fills the one panel of a board. Its value names the popup by `id`, the way
17097
- * `commandfor` does (several ids, space separated), because the exemption is
17098
- * one popup's: a menu open beside the board still closes on the same press.
17099
- *
17100
- * Unlike the outside marker it answers for its whole subtree: the outside
17101
- * marker carves free space out of a surface whose controls stay surface, this
17102
- * one adds a whole thing — the card, its count, its buttons — to the popup's
17103
- * own ground, and nothing in it is a dismissal.
17104
- */
17105
- const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
17106
-
17107
- /**
17108
- * `data-navi-popup-group` names the peers of a popup: the triggers of the other
17109
- * popups a press goes to from this one — a menu bar, where pressing another
17110
- * menu while one is open switches to it, in that one press. A trigger joins by
17111
- * carrying the name (a Picker's `popupGroup` writes it on its own), and a popup
17112
- * reads it off what the press landed on.
17113
- *
17114
- * Returns the peer `target` is in, or null.
17115
- */
17116
- const findPopupPeer = (target, group) => {
17117
- if (!group || !target || typeof target.closest !== "function") {
17118
- return null;
17119
- }
17120
- return target.closest(`[data-navi-popup-group="${CSS.escape(group)}"]`);
17121
- };
17122
-
17123
- /**
17124
- * The click a wall took, handed to the peer (see findPopupPeer) it covered.
17125
- *
17126
- * A wall cannot let one press through and not another: it is what the pointer
17127
- * hits, and that is the whole of how it spends a press. So it goes on taking
17128
- * every one — the page behind stays as unhoverable and unpressable as it looks
17129
- * — and once the press is over, its click is re-aimed at what the wall stood
17130
- * over at that point, when that is a peer. The click alone, for the reason
17131
- * transition_press.js gives: a pointer stream made up with no pointer behind it
17132
- * starts gestures (a hold, a drag) that nothing ever ends, and a click is what
17133
- * opens a popup's trigger.
17134
- *
17135
- * Called with the click that takes the wall down after a press closed the
17136
- * popup (see armPointerDownOutsideClose), so the peer opens as the popup it
17137
- * replaces goes.
17138
- */
17139
- const handWallClickToPeer = (
17140
- clickEvent,
17141
- { wallEl, popupEl, anchorEl, group },
17142
- ) => {
17143
- if (!clickEvent.isTrusted || clickEvent.target !== wallEl) {
17144
- // A click dispatched by hand is already aimed, and one that did not land
17145
- // on the wall (a press on the popup's own outside region) was no wall's.
17146
- return;
17147
- }
17148
- // The topmost element under the wall at that point: what the press would
17149
- // have landed on with no wall. Asked past the wall rather than after hiding
17150
- // it, which a fading wall may not be yet.
17151
- const covered = document
17152
- .elementsFromPoint(clickEvent.clientX, clickEvent.clientY)
17153
- .find((el) => el !== wallEl && !popupEl.contains(el));
17154
- const peer = findPopupPeer(covered, group);
17155
- if (!peer) {
17156
- return;
17157
- }
17158
- if (peer.contains(popupEl) || (anchorEl && peer.contains(anchorEl))) {
17159
- // The popup's own trigger — the one holding it (a picker renders its
17160
- // popup inside itself) or the one it opened against: the press through
17161
- // the wall closed the popup, and handing the trigger its click would open
17162
- // it again.
17163
- return;
17164
- }
17165
- // The page sees one click, on the peer, rather than two.
17166
- clickEvent.stopPropagation();
17167
- // A PointerEvent where the browser makes clicks of them, a MouseEvent
17168
- // elsewhere: the same kind, copied field by field.
17169
- const ClickEvent = clickEvent.constructor;
17170
- covered.dispatchEvent(new ClickEvent(clickEvent.type, clickEvent));
17171
- };
17172
-
17173
- /**
17174
- * What a press landing on a region the caller declared as not-its-surface
17175
- * does: exactly what the same press on the backdrop would do.
17176
- *
17177
- * Lives on the popup's own content element rather than in the backdrop's
17178
- * handler, for both renderers: the backdrop is a sibling behind the popup
17179
- * (never an ancestor), so a press inside the popup's box never reaches it.
17180
- */
17181
- const handlePressOnOutsideRegion = (
17182
- mouseDownEvent,
17183
- { popupEl, openController, pressOutside },
17184
- ) => {
17185
- if (mouseDownEvent.button !== 0) {
17186
- return;
17187
- }
17188
- const { target } = mouseDownEvent;
17189
- if (!target.hasAttribute(OUTSIDE_REGION_ATTRIBUTE)) {
17190
- return;
17191
- }
17192
- if (openedDuringThisPress(openController)) {
17193
- // The release of the press that opened it is not somebody dismissing it
17194
- // (see openedDuringThisPress).
17195
- return;
17196
- }
17197
- // A popup opens inside its opener's own subtree, so a press on a region
17198
- // belonging to a popup nested in this one bubbles through here too — and it
17199
- // is a press on what is in front, not on this popup's own decoration.
17200
- if (
17201
- target.closest(`[navi-control="dialog"], [navi-control="popover"]`) !==
17202
- popupEl
17203
- ) {
17204
- return;
17205
- }
17206
- if (pressOutside === "capture") {
17207
- mouseDownEvent.preventDefault();
17208
- return;
17209
- }
17210
- if (pressOutside === "close" || pressOutside === "cancel") {
17211
- openController.requestClose(mouseDownEvent, {
17212
- isCancel: pressOutside === "cancel",
17213
- });
17214
- }
17215
- };
17216
-
17217
- /**
17218
- * Hears an outside press from the document itself, for a popup with no
17219
- * backdrop of its own to catch one: a modal `<dialog>` (whose native
17220
- * `::backdrop` dispatches nothing a handler can be put on) and any popup the
17221
- * caller asked to leave the page reachable (`backdrop={false}`).
17222
- *
17223
- * The press is read and never taken: no `preventDefault`, no
17224
- * `stopPropagation`, and the listener is on `document` rather than on
17225
- * anything covering the page — so what the press landed on hears it too, in
17226
- * the same gesture, and the popup only takes note. That is the whole point of
17227
- * a popup with no wall: a press outside is a press on the page, and the page
17228
- * answers it. The one popup where that cannot hold is the modal dialog, where
17229
- * the browser has already made everything behind inert before this runs.
17230
- *
17231
- * `pressEventType` says which event IS the press, and the two callers want
17232
- * opposite ones:
17233
- *
17234
- * - `"pointerdown"` for a popup with no wall: a page with no wall over it is
17235
- * a page whose own elements arbitrate their presses, and cancelling a
17236
- * `pointerdown` — what a drag source and a control keeping the focus where
17237
- * it is both do — suppresses every compatibility mouse event that would have
17238
- * followed it. The press happens, reaches its target, and no `mousedown` is
17239
- * ever dispatched for it. Reading the pointer event is reading the press
17240
- * itself, whatever anything does with it afterwards, and it is the same
17241
- * event the drag and surface detectors read.
17242
- * - `"mousedown"` for the modal dialog, whose native `::backdrop` exists to
17243
- * spend the press. The page's controls act on `mousedown`, and on a touch
17244
- * screen that is not the same moment as `pointerdown`: the compatibility
17245
- * mouse events are synthesized at `touchend` and hit-tested again then.
17246
- * Closing on `pointerdown` takes the `::backdrop` away before that second
17247
- * hit-test, so the `mousedown` lands on whatever the page holds under the
17248
- * finger, and one tap both dismisses the dialog and presses the page. Read
17249
- * on `mousedown`, the wall is still up when the press the page acts on is
17250
- * aimed, and it catches it. Nothing behind a modal wall can cancel that
17251
- * press, since nothing behind it hears the `pointerdown` at all.
17252
- *
17253
- * Capture phase, so a handler downstream that stops propagation cannot keep
17254
- * the popup open either — the press is still outside whatever is made of it.
17255
- *
17256
- * Returns a cleanup that removes the listener.
17257
- */
17258
- const armOutsidePressClose = (
17259
- popupEl,
17260
- { openController, pressOutside, pressEventType = "pointerdown" },
17261
- ) => {
17262
- const onDocumentPointerDown = (pointerDownEvent) => {
17263
- if (pointerDownEvent.button !== 0) {
17264
- return;
17265
- }
17266
- if (openedDuringThisPress(openController)) {
17267
- // The release of the press that opened it is not somebody dismissing it
17268
- // (see openedDuringThisPress).
17269
- return;
17270
- }
17271
- // The press landed inside another popup: that is a press on what is in
17272
- // front, not outside. Asking the target where it lives rather than asking
17273
- // this popup whether it was pushed — a popup in front does not have to be
17274
- // one this popup knows about. In front means neither inside nor around: a
17275
- // popup nested in this one falls to the containment check below as the
17276
- // inside press it is, and one this popup opened INSIDE (a bubble rendered
17277
- // in a sheet's content) is around it, so what that popup holds is outside
17278
- // this one exactly like the rest of the page.
17279
- const popupUnderPointer = pointerDownEvent.target.closest?.(
17280
- `[navi-control="dialog"], [navi-control="popover"]`,
17281
- );
17282
- if (
17283
- popupUnderPointer &&
17284
- popupUnderPointer !== popupEl &&
17285
- !popupEl.contains(popupUnderPointer) &&
17286
- !popupUnderPointer.contains(popupEl)
17287
- ) {
17288
- return;
17289
- }
17290
- if (
17291
- popupEl.id &&
17292
- pointerDownEvent.target.closest?.(
17293
- `[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupEl.id)}"]`,
17294
- )
17295
- ) {
17296
- return;
17297
- }
17298
- // Real DOM containment wins over the rectangle below — an element
17299
- // genuinely inside the popup (`overflow: visible`, a negative margin, an
17300
- // absolutely-positioned child) can be painted outside its border box, and
17301
- // a press there is not outside just because its coordinates are. The
17302
- // popup element itself is excluded (contains() answers true for it): a
17303
- // press on a modal dialog's native backdrop reports the dialog as its
17304
- // target, there being no `::backdrop` node to be one, so treating that as
17305
- // contained would keep the rectangle from ever running.
17306
- if (
17307
- pointerDownEvent.target !== popupEl &&
17308
- popupEl.contains(pointerDownEvent.target)
17309
- ) {
17310
- return;
17311
- }
17312
- const rect = popupEl.getBoundingClientRect();
17313
- const isOutside =
17314
- pointerDownEvent.clientX < rect.left ||
17315
- pointerDownEvent.clientX > rect.right ||
17316
- pointerDownEvent.clientY < rect.top ||
17317
- pointerDownEvent.clientY > rect.bottom;
17318
- if (!isOutside) {
17319
- return;
17320
- }
17321
- openController.requestClose(pointerDownEvent, {
17322
- isCancel: pressOutside === "cancel",
17323
- });
17324
- };
17325
- document.addEventListener(pressEventType, onDocumentPointerDown, {
17326
- capture: true,
17327
- });
17328
- return () => {
17329
- document.removeEventListener(pressEventType, onDocumentPointerDown, {
17330
- capture: true,
17331
- });
17332
- };
17333
- };
17334
-
17335
16851
  /**
17336
16852
  * Typography reset shared by the surfaces a control opens: callout, popover,
17337
16853
  * dialog.
@@ -17454,7 +16970,7 @@ installImportMetaCssBuild(import.meta);
17454
16970
  * (see calloutTemplate in callout.js).
17455
16971
  */
17456
16972
 
17457
- const css$15 = /* css */`.navi_callout_status_icon {
16973
+ const css$16 = /* css */`.navi_callout_status_icon {
17458
16974
  --x-callout-status-icon-color: var(--navi-callout-neutral-color);
17459
16975
  box-sizing: border-box;
17460
16976
  aspect-ratio: 1;
@@ -17525,7 +17041,7 @@ const CalloutStatusIcon = ({
17525
17041
  status = "info",
17526
17042
  shape = "square"
17527
17043
  }) => {
17528
- import.meta.css = [css$15, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
17044
+ import.meta.css = [css$16, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
17529
17045
  return jsx("span", {
17530
17046
  className: "navi_callout_status_icon",
17531
17047
  "data-status": status === "none" ? undefined : status,
@@ -17566,11 +17082,13 @@ const SPENT_PRESS_EVENT_TYPES = [
17566
17082
  "contextmenu",
17567
17083
  ];
17568
17084
  // Where a press lands on something drawn over the page rather than on the page
17569
- // itself: a popup of navi's (either layer) or a native top-layer element.
17085
+ // itself: a popup of navi's (either layer) or a native top-layer element. An
17086
+ // idle PopupGroup is a closed <dialog> that is part of the page (see
17087
+ // popup_group.jsx).
17570
17088
  const LAYER_SELECTOR =
17571
- '[popover], dialog, [navi-control="popover"], [navi-control="dialog"]';
17089
+ '[popover], dialog:not([navi-popup-group]:not(:modal)), [navi-control="popover"], [navi-control="dialog"]';
17572
17090
 
17573
- const css$14 = /* css */ `
17091
+ const css$15 = /* css */ `
17574
17092
  /* jsenv-css-opaque: same as dialog.jsx, cascade order. */
17575
17093
  @layer navi {
17576
17094
  .navi_callout {
@@ -17843,6 +17361,10 @@ const css$14 = /* css */ `
17843
17361
  * If not provided or too big, callout will be centered in viewport. It says where the
17844
17362
  * callout is drawn, not what dismisses it: see `openingEvent` for what a container anchor
17845
17363
  * costs
17364
+ * @param {HTMLElement} [options.containerElement] - Where the callout element goes in the
17365
+ * DOM, when that is not where the anchor puts it (the anchor itself, or beside it). What
17366
+ * holds a callout decides what it inherits and whether it can be reached at all: outside
17367
+ * an engaged PopupGroup it is inert. The anchor still says where it points
17846
17368
  * @param {string} [options.status=""] - Callout status: "info" | "warning" | "error" | "success"
17847
17369
  * @param {string} [options.testId] - `data-testid` on the callout element. The callout is
17848
17370
  * drawn by navi, so nothing the caller renders can carry the name a test needs — same
@@ -17864,10 +17386,6 @@ const css$14 = /* css */ `
17864
17386
  * still scrolls, a press on a layer drawn over the page (a sheet the callout's content
17865
17387
  * opened) is that layer's, and a press that does not close the callout (an "error" one) is
17866
17388
  * not spent either
17867
- * @param {string} [options.group] - With `backdrop`: the name the callout's peers carry in
17868
- * `data-navi-popup-group` — the triggers of other popups, callouts or not. A press inside a
17869
- * peer still closes the callout but is not spent, so the peer it landed on opens its own
17870
- * popup in the same press — a menu bar, where pressing another menu switches to it
17871
17389
  * @param {Event} [options.openingEvent] - The gesture the callout belongs to. It names the
17872
17390
  * opener — `currentTarget` while the event is still dispatching, `target` once it is over,
17873
17391
  * so awaiting before opening changes nothing — and the opener is the one part of the anchor
@@ -17928,12 +17446,12 @@ const openCallout = (
17928
17446
  // "" - neutral information
17929
17447
  status = "",
17930
17448
  testId,
17449
+ containerElement,
17931
17450
  onClose,
17932
17451
  closeByPressOutside = status === "info",
17933
17452
  closeOnFocusLeave = closeByPressOutside,
17934
17453
  closeByScroll = false,
17935
17454
  backdrop = false,
17936
- group,
17937
17455
  closeButton = true,
17938
17456
  openingEvent,
17939
17457
  reopen = "toggle",
@@ -17943,7 +17461,7 @@ const openCallout = (
17943
17461
  debug = () => {},
17944
17462
  } = {},
17945
17463
  ) => {
17946
- import.meta.css = [css$14, "@jsenv/navi/src/control/rules/callout/callout.js"];
17464
+ import.meta.css = [css$15, "@jsenv/navi/src/control/rules/callout/callout.js"];
17947
17465
  if (debug === true) {
17948
17466
  debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
17949
17467
  }
@@ -18245,10 +17763,11 @@ const openCallout = (
18245
17763
  }
18246
17764
  }
18247
17765
  const calloutContainer = (() => {
18248
- if (!anchorElement || anchorElement === document.body) {
17766
+ const holder = containerElement || anchorElement;
17767
+ if (!holder || holder === document.body) {
18249
17768
  return document.body;
18250
17769
  }
18251
- const closestLabel = anchorElement.closest("label");
17770
+ const closestLabel = holder.closest("label");
18252
17771
  if (closestLabel) {
18253
17772
  // Putting callout inside label would be problematic:
18254
17773
  // - Click on callout would be catched by the browser and re-dispatched to the label
@@ -18260,10 +17779,10 @@ const openCallout = (
18260
17779
  return labelParent;
18261
17780
  }
18262
17781
  // Some elements (e.g. <input>) cannot have children
18263
- if (canContainCallout(anchorElement)) {
18264
- return anchorElement;
17782
+ if (canContainCallout(holder)) {
17783
+ return holder;
18265
17784
  }
18266
- return anchorElement.parentNode || document.body;
17785
+ return holder.parentNode || document.body;
18267
17786
  })();
18268
17787
 
18269
17788
  {
@@ -18310,8 +17829,7 @@ const openCallout = (
18310
17829
  // Whether an outside press on `target` is spent on closing the callout
18311
17830
  // (`backdrop`). What is not the page behind the callout keeps its press: a
18312
17831
  // layer the callout is not part of — its own page may well be a sheet, a
18313
- // sheet its content opened is in front of it — and a peer, which the press
18314
- // is meant to open.
17832
+ // sheet its content opened is in front of it.
18315
17833
  const spendsPressOn = (target) => {
18316
17834
  if (!backdrop || !pressOutsideCloses()) {
18317
17835
  return false;
@@ -18320,9 +17838,6 @@ const openCallout = (
18320
17838
  if (layer && !layer.contains(calloutElement)) {
18321
17839
  return false;
18322
17840
  }
18323
- if (findPopupPeer(target, group)) {
18324
- return false;
18325
- }
18326
17841
  return true;
18327
17842
  };
18328
17843
  const spend = (event) => {
@@ -19538,7 +19053,7 @@ const generateSvgWithoutArrow = (width, height) => {
19538
19053
  *
19539
19054
  * Usage:
19540
19055
  * const myToken = createOpenToken();
19541
- * calloutManager.addOpenToken(myToken, { message, status, testId, closeByScroll, backdrop, group, anchorElement, event, skipFocus, onClose });
19056
+ * calloutManager.addOpenToken(myToken, { message, status, testId, closeByScroll, backdrop, closeByPressOutside, containerElement, anchorElement, event, skipFocus, onClose });
19542
19057
  * calloutManager.removeOpenToken(myToken, event);
19543
19058
  * calloutManager.requestCloseCallout(event, debugReason); // force-close all
19544
19059
  * calloutManager.callout // current open callout or null
@@ -19594,10 +19109,15 @@ const createCalloutManager = (
19594
19109
  testId: tokenData.testId,
19595
19110
  icon: tokenData.icon,
19596
19111
  closeButton: tokenData.closeButton,
19597
- closeByPressOutside: tokenData.status !== "error",
19112
+ // Unless the token says otherwise: what a press outside a picker's
19113
+ // callout does inside a PopupGroup is the group's to decide. Focus
19114
+ // leaving stays the callout's own way out, whoever answers the press.
19115
+ closeByPressOutside:
19116
+ tokenData.closeByPressOutside ?? tokenData.status !== "error",
19117
+ closeOnFocusLeave: tokenData.status !== "error",
19118
+ containerElement: tokenData.containerElement,
19598
19119
  closeByScroll: tokenData.closeByScroll,
19599
19120
  backdrop: tokenData.backdrop,
19600
- group: tokenData.group,
19601
19121
  anchorElement,
19602
19122
  openingEvent: event,
19603
19123
  skipFocus: tokenData.skipFocus,
@@ -19727,7 +19247,8 @@ const createCalloutManager = (
19727
19247
  closeButton,
19728
19248
  closeByScroll,
19729
19249
  backdrop,
19730
- group,
19250
+ closeByPressOutside,
19251
+ containerElement,
19731
19252
  anchorElement,
19732
19253
  event,
19733
19254
  skipFocus,
@@ -19746,7 +19267,8 @@ const createCalloutManager = (
19746
19267
  closeButton,
19747
19268
  closeByScroll,
19748
19269
  backdrop,
19749
- group,
19270
+ closeByPressOutside,
19271
+ containerElement,
19750
19272
  skipFocus,
19751
19273
  onClose,
19752
19274
  // Resolved as the token is added, and kept with it: a token shown later
@@ -22066,7 +21588,10 @@ const readNumberFromDom = (element, attribute, defaultValue) => {
22066
21588
  * what a popup outside the top layer says for itself).
22067
21589
  */
22068
21590
 
22069
- const LAYER_OVER_SELECTOR = "[data-drag-ignore],[popover],dialog";
21591
+ // An idle PopupGroup is a closed <dialog> that is part of the page, not a layer
21592
+ // over it (see popup_group.jsx).
21593
+ const LAYER_OVER_SELECTOR =
21594
+ "[data-drag-ignore],[popover],dialog:not([navi-popup-group]:not(:modal))";
22070
21595
 
22071
21596
  /**
22072
21597
  * The nearest word wins: a layer INSIDE the element takes the press away from
@@ -22245,7 +21770,7 @@ import.meta.css = /* css */ [`@property --swipe-progress {
22245
21770
  user-select: none;
22246
21771
  }
22247
21772
 
22248
- :is([data-longpress], [data-swipe], [data-double-click]) :is([data-drag-ignore], [popover], dialog), :is([data-longpress], [data-swipe], [data-double-click]) :is(input:not([data-press-only]), textarea), :is([data-longpress], [data-swipe], [data-double-click]) :is([contenteditable=""], [contenteditable="true"]) {
21773
+ :is([data-longpress], [data-swipe], [data-double-click]) :is([data-drag-ignore], [popover], dialog:not([navi-popup-group]:not(:modal))), :is([data-longpress], [data-swipe], [data-double-click]) :is(input:not([data-press-only]), textarea), :is([data-longpress], [data-swipe], [data-double-click]) :is([contenteditable=""], [contenteditable="true"]) {
22249
21774
  user-select: text;
22250
21775
  }
22251
21776
 
@@ -33184,7 +32709,7 @@ const keepLeavingPages = () => {
33184
32709
  };
33185
32710
  };
33186
32711
  const LEAVING_ATTRIBUTE = "data-navi-route-leaving";
33187
- const css$13 = /* css */`[data-navi-route-leaving] {
32712
+ const css$14 = /* css */`[data-navi-route-leaving] {
33188
32713
  display: none !important;
33189
32714
  }
33190
32715
  `;
@@ -33456,7 +32981,7 @@ const useContentKeepingLeavingPages = (branch, readShownBranch, parentPage) => {
33456
32981
  if (leaving.length === 0) {
33457
32982
  return vnode ? [vnode] : null;
33458
32983
  }
33459
- import.meta.css = [css$13, "@jsenv/navi/src/nav/route.jsx"];
32984
+ import.meta.css = [css$14, "@jsenv/navi/src/nav/route.jsx"];
33460
32985
  const children = vnode ? [vnode] : [];
33461
32986
  for (const entry of leaving) {
33462
32987
  children.push(h("template", {
@@ -33951,20 +33476,21 @@ const TOP_LAYER_SELECTOR = ":modal, :popover-open";
33951
33476
  // stands over the pages in the top layer. A sticky row inside the pages needs
33952
33477
  // none of this — it lives in the area, so it is already part of the pages' own
33953
33478
  // picture, and so is a popup rendered with layer="local".
33954
- const FURNITURE_SELECTOR = `.navi_fixed_bar, .navi_popover:is(${TOP_LAYER_SELECTOR}), .navi_dialog:is(${TOP_LAYER_SELECTOR})`;
33479
+ const FURNITURE_SELECTOR = `.navi_fixed_bar, .navi_popover:is(${TOP_LAYER_SELECTOR}), .navi_dialog:is(${TOP_LAYER_SELECTOR}), .navi_popup_group:is(${TOP_LAYER_SELECTOR})`;
33955
33480
  // Worn by the root for the length of a route transition (route_transition.jsx
33956
33481
  // owns it). Written out rather than imported: importing the module that owns
33957
33482
  // it back into this one would close a cycle.
33958
33483
  const TRANSITION_ATTRIBUTE$1 = "data-navi-route-transition";
33959
33484
  const NAME_PROPERTY$1 = "view-transition-name";
33960
33485
  const FURNITURE_NAME_PREFIX = "navi-transition-furniture-";
33961
- // The walls painted in the top layer: a modal dialog's, which is its
33962
- // ::backdrop, and a popover's, which is a real element shown as a popover
33486
+ // The walls painted in the top layer: a modal dialog's (a PopupGroup's too,
33487
+ // layout/popup_group.jsx), which is its ::backdrop, and a popover's, which is
33488
+ // a real element shown as a popover
33963
33489
  // beside it (layout/popover.jsx) — unnamed, so covered by the pictures for
33964
33490
  // the length of the movement all the same. A top-layer dialog without a wall
33965
33491
  // is shown as a popover and has nothing to stand in for.
33966
33492
  const TOP_LAYER_WALL_SELECTOR =
33967
- ".navi_dialog:modal, .navi_popover_backdrop:popover-open";
33493
+ ".navi_dialog:modal, .navi_popover_backdrop:popover-open, .navi_popup_group:modal";
33968
33494
  const WALL_ATTRIBUTE = "data-navi-transition-wall";
33969
33495
  // Which side of the movement a bar is on, worn by the bar and read by the
33970
33496
  // browser as the class of its group (the CSS below): "leaving" from the first
@@ -34026,7 +33552,7 @@ const TRANSITION_FURNITURE_CSS = /* css */ `.navi_fixed_bar[data-navi-transition
34026
33552
  }
34027
33553
 
34028
33554
  :root[data-navi-route-transition-target="area"] {
34029
- & .navi_dialog::backdrop, & .navi_popover_backdrop {
33555
+ & .navi_dialog::backdrop, & .navi_popup_group::backdrop, & .navi_popover_backdrop {
34030
33556
  backdrop-filter: none;
34031
33557
  background: none;
34032
33558
  }
@@ -35127,7 +34653,7 @@ const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
35127
34653
  // the root pictures must NOT move (they carry the whole viewport, blank bands
35128
34654
  // included).
35129
34655
 
35130
- const css$12 = /* css */`@property --navi-route-transition-leave {
34656
+ const css$13 = /* css */`@property --navi-route-transition-leave {
35131
34657
  syntax: "*";
35132
34658
  inherits: false
35133
34659
  }
@@ -36709,7 +36235,7 @@ const describePage = ({
36709
36235
  // is the application's element as often as <RouteTransitionArea>, and is
36710
36236
  // only found as a movement starts.
36711
36237
  const installRouteTransitionCss = () => {
36712
- import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
36238
+ import.meta.css = [css$13, "@jsenv/navi/src/nav/route_transition.jsx"];
36713
36239
  installTransitionWindowCss();
36714
36240
  installTransitionFurnitureCss();
36715
36241
  };
@@ -36746,7 +36272,7 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
36746
36272
  const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
36747
36273
  // The name the box wears while it travels, and only then (see nameForTravel).
36748
36274
  const TRAVEL_NAME = "navi-route-travel";
36749
- const css$11 = /* css */`.navi_route_travel {
36275
+ const css$12 = /* css */`.navi_route_travel {
36750
36276
  position: relative;
36751
36277
 
36752
36278
  &[data-travel-by-drag="x"] {
@@ -37044,7 +36570,7 @@ const RouteTravel = ({
37044
36570
  children,
37045
36571
  ...rest
37046
36572
  }) => {
37047
- import.meta.css = [css$11, "@jsenv/navi/src/nav/route_travel.jsx"];
36573
+ import.meta.css = [css$12, "@jsenv/navi/src/nav/route_travel.jsx"];
37048
36574
  installTransitionWindowCss();
37049
36575
  const elementRef = useRef();
37050
36576
  const gestureRef = useRef(null);
@@ -40444,6 +39970,504 @@ const NAVI_COMMANDS = {
40444
39970
  "--navi-unselect": unselectCommand,
40445
39971
  };
40446
39972
 
39973
+ /**
39974
+ * Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
39975
+ * (non-top-layer) renderers — operate on a plain DOM element, no knowledge
39976
+ * of which of the two owns it.
39977
+ */
39978
+
39979
+
39980
+ /**
39981
+ * Whether a visibleRectEffect delivery is one that can have taken height away
39982
+ * from a popup, and so pushed whatever holds focus out of sight:
39983
+ * - "resize": the window/visual viewport settled — which is also how the
39984
+ * on-screen keyboard arrives, overlay or not (window_size.js);
39985
+ * - ELEMENT_SIZE_CHANGE: the popup's own box measured different;
39986
+ * - "focusin": the focus-settled re-measure, for the room that changes with
39987
+ * nothing announcing it (subscribeFocusSettled in window_size.js).
39988
+ *
39989
+ * Everything else is a scroll of one kind or another, where nothing got
39990
+ * smaller and scrolling the focused element back would fight the very gesture
39991
+ * that fired it.
39992
+ */
39993
+ const mayHaveHiddenFocus = (event) => {
39994
+ const type = event?.type;
39995
+ return (
39996
+ type === "resize" || type === ELEMENT_SIZE_CHANGE || type === "focusin"
39997
+ );
39998
+ };
39999
+
40000
+ const scrollportHeightMap = new WeakMap();
40001
+ /**
40002
+ * Scrolls whatever holds focus inside `popupEl` back into view, if the popup
40003
+ * getting shorter has pushed it out.
40004
+ *
40005
+ * The case this exists for: a field low in the scrolling body of a popup that
40006
+ * also has a footer (box.jsx — with a body, the body is the only thing that
40007
+ * scrolls and the footer is a sibling sitting right under it). Focusing the
40008
+ * field makes the browser scroll it into view, which it does against the
40009
+ * popup's height AT THAT MOMENT; the on-screen keyboard then opens and takes
40010
+ * that height away. The body shrinks, its scrollTop does not move, so the
40011
+ * content slides down relative to the shorter scrollport and the field ends up
40012
+ * past its bottom edge — visually, swallowed by the footer. The browser does
40013
+ * not redo a scroll-into-view it already answered, so this does.
40014
+ *
40015
+ * Only what the shrink itself hid, though: with the keyboard up the user reads
40016
+ * the rest of the popup by scrolling the field away — to reach the submit under
40017
+ * it, typically — and the room keeps changing while they do (a keyboard settling
40018
+ * in two steps, a suggestion strip, a browser bar). Answering each of those by
40019
+ * scrolling the field back takes the popup away from wherever they had just
40020
+ * scrolled it, over and over: what they were reading cannot be reached at all
40021
+ * without blurring the field first, and the popup reads as unscrollable. So the
40022
+ * field is brought back only when it was in view before the room shrank, which
40023
+ * one remembered number answers: a resize moves neither scrollTop nor the
40024
+ * element's offset inside the scrolled content, so measuring against the height
40025
+ * the scrollport HAD is measuring the state before the change.
40026
+ *
40027
+ * Scoped to the field's own scroll container (never the page): a popup traps
40028
+ * scrolling precisely so the document underneath cannot move, and a plain
40029
+ * scrollIntoView walks past a container whose scrollbar isn't visible — see
40030
+ * scrollIntoViewScoped's own doc.
40031
+ *
40032
+ * "nearest": the smallest scroll that makes it visible, and none at all when it
40033
+ * already is. Where it lands is the container's own business — a navi scroller
40034
+ * keeps a band free at its edges so a field never comes back glued to one (see
40035
+ * scroll-padding in box.jsx).
40036
+ */
40037
+ const keepFocusedElementVisible = (popupEl) => {
40038
+ const { activeElement } = document;
40039
+ if (!activeElement || activeElement === popupEl) {
40040
+ return;
40041
+ }
40042
+ if (!popupEl.contains(activeElement)) {
40043
+ return;
40044
+ }
40045
+ const scrollContainer = getScrollContainer(activeElement);
40046
+ if (!scrollContainer || !popupEl.contains(scrollContainer)) {
40047
+ // What scrolls the field is outside the popup, which means the page: a
40048
+ // popup holds it still on purpose (trapScrollInside), so there is nothing
40049
+ // here to scroll back.
40050
+ return;
40051
+ }
40052
+ const scrollportHeight = scrollContainer.clientHeight;
40053
+ const scrollportHeightBefore = scrollportHeightMap.get(scrollContainer);
40054
+ scrollportHeightMap.set(scrollContainer, scrollportHeight);
40055
+ if (scrollportHeightBefore !== undefined) {
40056
+ if (scrollportHeight >= scrollportHeightBefore) {
40057
+ // Nothing was taken away, so nothing was hidden by this.
40058
+ return;
40059
+ }
40060
+ if (
40061
+ !isVisibleInScrollport(
40062
+ activeElement,
40063
+ scrollContainer,
40064
+ scrollportHeightBefore,
40065
+ )
40066
+ ) {
40067
+ return;
40068
+ }
40069
+ }
40070
+ scrollIntoViewScoped(activeElement, {
40071
+ container: scrollContainer,
40072
+ block: "nearest",
40073
+ });
40074
+ };
40075
+
40076
+ // Whether any part of `el` was showing in `container` back when its scrollport
40077
+ // was `scrollportHeight` tall. Both boxes are read now: the container may have
40078
+ // moved as well as shrunk (a centered dialog re-centers itself), and the
40079
+ // difference between the two tops is what that move leaves alone.
40080
+ const isVisibleInScrollport = (el, container, scrollportHeight) => {
40081
+ const elRect = el.getBoundingClientRect();
40082
+ const containerRect = container.getBoundingClientRect();
40083
+ const elTop = elRect.top - containerRect.top + container.scrollTop;
40084
+ const elBottom = elTop + elRect.height;
40085
+ const scrollportTop = container.scrollTop;
40086
+ const scrollportBottom = scrollportTop + scrollportHeight;
40087
+ return elBottom > scrollportTop && elTop < scrollportBottom;
40088
+ };
40089
+
40090
+ /**
40091
+ * Calls `onSettled` once `el`'s current CSS transition is over — via
40092
+ * `transitionend`, with a safety `setTimeout` fallback matching the longest
40093
+ * `transition-duration`, in case nothing actually transitions or an event is
40094
+ * missed.
40095
+ *
40096
+ * Returns a "cancel" function, so a caller whose instance has been superseded
40097
+ * (a fresh open/close about to set its own state) can keep this stale one from
40098
+ * firing later. Cancelling only stops `onSettled`: undoing whatever the caller
40099
+ * did up front is that fresh call's business, not this one's.
40100
+ */
40101
+ const whenTransitionSettles = (el, onSettled) => {
40102
+ let settled = false;
40103
+ const onTransitionEnd = (transitionEvent) => {
40104
+ if (transitionEvent.target === el) {
40105
+ finish();
40106
+ }
40107
+ };
40108
+ const stopWatching = () => {
40109
+ settled = true;
40110
+ el.removeEventListener("transitionend", onTransitionEnd);
40111
+ clearTimeout(safetyTimeoutId);
40112
+ };
40113
+ const finish = () => {
40114
+ if (settled) {
40115
+ return;
40116
+ }
40117
+ stopWatching();
40118
+ onSettled();
40119
+ };
40120
+ el.addEventListener("transitionend", onTransitionEnd);
40121
+ const durationsInSeconds = getComputedStyle(el)
40122
+ .transitionDuration.split(",")
40123
+ .map((value) => parseFloat(value) || 0);
40124
+ const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
40125
+ const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
40126
+ return () => {
40127
+ if (settled) {
40128
+ return;
40129
+ }
40130
+ stopWatching();
40131
+ };
40132
+ };
40133
+
40134
+ /**
40135
+ * Drops the document's text selection when it lives inside `el`, leaving a
40136
+ * selection made elsewhere on the page alone.
40137
+ *
40138
+ * A popup being closed takes its content with it, and a selection is a claim
40139
+ * on content the user can still act on: kept, it would outlive the surface it
40140
+ * was made on — painted on the box for the length of its exit transition, with
40141
+ * the handles and the copy toolbar a phone draws from the live selection
40142
+ * hanging over something that is going away — and reappear with the box on
40143
+ * the next opening. Dropping the selection is the only way to remove that
40144
+ * chrome: a `user-select: none` on the closing box hides the highlight in some
40145
+ * browsers only, and the handles are the browser's own, drawn from the
40146
+ * selection object rather than from any style.
40147
+ */
40148
+ const clearTextSelectionInside = (el) => {
40149
+ const selection = window.getSelection();
40150
+ if (!selection || selection.rangeCount === 0) {
40151
+ return;
40152
+ }
40153
+ const range = selection.getRangeAt(0);
40154
+ if (!el.contains(range.commonAncestorContainer)) {
40155
+ return;
40156
+ }
40157
+ selection.removeAllRanges();
40158
+ };
40159
+
40160
+ /**
40161
+ * Disables pointer-events on `el` until its current CSS transition settles —
40162
+ * avoids the cursor changing/something becoming clickable while the popup is
40163
+ * still visually moving into or out of place.
40164
+ *
40165
+ * Returns whenTransitionSettles' own "cancel" function: it doesn't restore
40166
+ * pointer-events, since a fresh call for the next open/close is about to set
40167
+ * its own state.
40168
+ */
40169
+ const suppressPointerEventsDuringTransition = (el) => {
40170
+ el.style.pointerEvents = "none";
40171
+ return whenTransitionSettles(el, () => {
40172
+ el.style.pointerEvents = "";
40173
+ });
40174
+ };
40175
+
40176
+ /**
40177
+ * Hides the backdrop, deferring until the browser's matching "click" fires
40178
+ * when `closeEvent` was triggered by a mousedown (see popover.jsx's top
40179
+ * comment for why) — same capture-phase-on-document pattern as
40180
+ * armSuppressNextOpenRequest in open_controller.js, which a plain timeout
40181
+ * can't safely replace: mouseup (and the click that follows it) can land an
40182
+ * arbitrarily long time after mousedown (the user is still holding the
40183
+ * button down), so a short timeout can fire first and hide the backdrop
40184
+ * before its own click ever arrives. A capture-phase listener on document
40185
+ * fires for every click regardless of what any bubble-phase handler does
40186
+ * downstream, so no fallback timer is needed.
40187
+ *
40188
+ * `hide` is the caller's own way to actually hide the backdrop
40189
+ * (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
40190
+ * for a plain div) — this helper only owns the mousedown/click timing.
40191
+ *
40192
+ * Returns a disarm function (or undefined if hidden immediately), so a
40193
+ * fresh open can cancel a pending hide it's about to make redundant.
40194
+ */
40195
+ const armPointerDownOutsideClose = (closeEvent, hide) => {
40196
+ const mousedownEvent = findEvent(closeEvent, "mousedown");
40197
+ if (!mousedownEvent) {
40198
+ hide();
40199
+ return undefined;
40200
+ }
40201
+ const onClick = () => {
40202
+ document.removeEventListener("click", onClick, { capture: true });
40203
+ hide();
40204
+ };
40205
+ document.addEventListener("click", onClick, { capture: true });
40206
+ return () => {
40207
+ document.removeEventListener("click", onClick, { capture: true });
40208
+ };
40209
+ };
40210
+
40211
+ /**
40212
+ * Maps a positionArea y/x pair to a concrete `navi-animation` value (a
40213
+ * `prefix` plus a direction word), or `null` if both axes overlap the anchor
40214
+ * (no direction at all — that's `resolvedAnimationKind === "scaling"`
40215
+ * territory instead, see resolveAutoAnimationKind below).
40216
+ *
40217
+ * `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
40218
+ * when docked) keeps the word as the compass direction the popup comes
40219
+ * from: placed "top" (a point/corner), it slides in from the top.
40220
+ * `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
40221
+ * direction instead, the opposite compass point: placed "top" of the
40222
+ * anchor, it moves/grows up, away from the anchor (which sits below it).
40223
+ *
40224
+ * "inset-*"/"center" contribute no direction on their axis either way.
40225
+ */
40226
+ const resolveDirectionValue = (y, x, { prefix }) => {
40227
+ const yWord =
40228
+ y === "top"
40229
+ ? prefix === "expand"
40230
+ ? "up"
40231
+ : "top"
40232
+ : y === "bottom"
40233
+ ? prefix === "expand"
40234
+ ? "down"
40235
+ : "bottom"
40236
+ : null;
40237
+ const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
40238
+ if (!yWord && !xWord) {
40239
+ return null;
40240
+ }
40241
+ return yWord && xWord
40242
+ ? `${prefix}-${yWord}-${xWord}`
40243
+ : `${prefix}-${yWord || xWord}`;
40244
+ };
40245
+
40246
+ /**
40247
+ * Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
40248
+ * — picked for any real anchor, or for a point/corner placed dead-center
40249
+ * (both positionArea axes overlapping — there's no sensible direction to
40250
+ * slide from in that case). "sliding" otherwise. `anchor` is `undefined`
40251
+ * for any no-anchor/docked case (Dialog always, Popover's own custom
40252
+ * renderer when there's no real anchor), so this collapses to "scaling"
40253
+ * there only for the dead-center case, "sliding" otherwise. The two
40254
+ * "overlapping" booleans below describe the *positionArea* itself (a bare
40255
+ * word vs. "inset-"/"center"), not anything about the anchor — they'd
40256
+ * mean exactly the same thing even with no anchor at all, since it's the
40257
+ * position strategy, not the anchor, that decides whether there's a
40258
+ * direction to slide from.
40259
+ */
40260
+ const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
40261
+ const yIsOverlapping =
40262
+ parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
40263
+ const xIsOverlapping =
40264
+ parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
40265
+ return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
40266
+ };
40267
+
40268
+ /**
40269
+ * A popup reads "outside" from its own border box: what a press lands on
40270
+ * decides nothing on its own (a genuine backdrop press and a press on the
40271
+ * popup's padding both report the popup element as their target, there being
40272
+ * no real ::backdrop node to be one), so the rectangle is what tells them
40273
+ * apart. That holds as long as the box and what the popup paints are the same
40274
+ * thing — which stops being true for a popup with no surface of its own
40275
+ * (`backgroundColor="transparent"`, no shadow, no padding): what the eye reads
40276
+ * as backdrop is then inside the box, and a press there stays a press on the
40277
+ * popup.
40278
+ *
40279
+ * `data-navi-popup-outside` is how a caller says which of its own boxes are
40280
+ * not the surface. It is opt-in because navi cannot infer it — a background
40281
+ * can come from anywhere — while the caller who made the popup see-through
40282
+ * knows exactly which box is decoration and which is paper.
40283
+ *
40284
+ * The marker answers for the element it is on, never for its descendants: a
40285
+ * box painted inside a marked one is still surface, so a row can be marked and
40286
+ * its empty halves read as backdrop while presses on the controls it holds do
40287
+ * not dismiss anything. A descendant with `pointer-events: none` never becomes
40288
+ * a press target at all, so the marked box answers in its place — while the
40289
+ * same declaration on the marked box itself takes it out of hit-testing and
40290
+ * makes the marker unreachable (see warnAboutUnreachableOutsideRegions).
40291
+ */
40292
+ const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
40293
+
40294
+ /**
40295
+ * `data-navi-popup-inside` is the other direction: a box of the PAGE the
40296
+ * caller says belongs to a popup, so a press on it is not outside that popup
40297
+ * even though it lies beyond the border box — a card whose press is what
40298
+ * fills the one panel of a board. Its value names the popup by `id`, the way
40299
+ * `commandfor` does (several ids, space separated), because the exemption is
40300
+ * one popup's: a menu open beside the board still closes on the same press.
40301
+ *
40302
+ * Unlike the outside marker it answers for its whole subtree: the outside
40303
+ * marker carves free space out of a surface whose controls stay surface, this
40304
+ * one adds a whole thing — the card, its count, its buttons — to the popup's
40305
+ * own ground, and nothing in it is a dismissal.
40306
+ */
40307
+ const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
40308
+ // Whether `target` is in a box of the page declared as part of the popup
40309
+ // named `popupId`.
40310
+ const isDeclaredInsidePopup = (target, popupId) =>
40311
+ Boolean(popupId) &&
40312
+ Boolean(target?.closest?.(`[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupId)}"]`));
40313
+
40314
+ /**
40315
+ * What a press landing on a region the caller declared as not-its-surface
40316
+ * does: exactly what the same press on the backdrop would do.
40317
+ *
40318
+ * Lives on the popup's own content element rather than in the backdrop's
40319
+ * handler, for both renderers: the backdrop is a sibling behind the popup
40320
+ * (never an ancestor), so a press inside the popup's box never reaches it.
40321
+ */
40322
+ const handlePressOnOutsideRegion = (
40323
+ mouseDownEvent,
40324
+ { popupEl, openController, pressOutside },
40325
+ ) => {
40326
+ if (mouseDownEvent.button !== 0) {
40327
+ return;
40328
+ }
40329
+ const { target } = mouseDownEvent;
40330
+ if (!target.hasAttribute(OUTSIDE_REGION_ATTRIBUTE)) {
40331
+ return;
40332
+ }
40333
+ if (openedDuringThisPress(openController)) {
40334
+ // The release of the press that opened it is not somebody dismissing it
40335
+ // (see openedDuringThisPress).
40336
+ return;
40337
+ }
40338
+ // A popup opens inside its opener's own subtree, so a press on a region
40339
+ // belonging to a popup nested in this one bubbles through here too — and it
40340
+ // is a press on what is in front, not on this popup's own decoration.
40341
+ if (
40342
+ target.closest(`[navi-control="dialog"], [navi-control="popover"]`) !==
40343
+ popupEl
40344
+ ) {
40345
+ return;
40346
+ }
40347
+ if (pressOutside === "capture") {
40348
+ mouseDownEvent.preventDefault();
40349
+ return;
40350
+ }
40351
+ if (pressOutside === "close" || pressOutside === "cancel") {
40352
+ openController.requestClose(mouseDownEvent, {
40353
+ isCancel: pressOutside === "cancel",
40354
+ });
40355
+ }
40356
+ };
40357
+
40358
+ /**
40359
+ * Hears an outside press from the document itself, for a popup with no
40360
+ * backdrop of its own to catch one: a modal `<dialog>` (whose native
40361
+ * `::backdrop` dispatches nothing a handler can be put on) and any popup the
40362
+ * caller asked to leave the page reachable (`backdrop={false}`).
40363
+ *
40364
+ * The press is read and never taken: no `preventDefault`, no
40365
+ * `stopPropagation`, and the listener is on `document` rather than on
40366
+ * anything covering the page — so what the press landed on hears it too, in
40367
+ * the same gesture, and the popup only takes note. That is the whole point of
40368
+ * a popup with no wall: a press outside is a press on the page, and the page
40369
+ * answers it. The one popup where that cannot hold is the modal dialog, where
40370
+ * the browser has already made everything behind inert before this runs.
40371
+ *
40372
+ * `pressEventType` says which event IS the press, and the two callers want
40373
+ * opposite ones:
40374
+ *
40375
+ * - `"pointerdown"` for a popup with no wall: a page with no wall over it is
40376
+ * a page whose own elements arbitrate their presses, and cancelling a
40377
+ * `pointerdown` — what a drag source and a control keeping the focus where
40378
+ * it is both do — suppresses every compatibility mouse event that would have
40379
+ * followed it. The press happens, reaches its target, and no `mousedown` is
40380
+ * ever dispatched for it. Reading the pointer event is reading the press
40381
+ * itself, whatever anything does with it afterwards, and it is the same
40382
+ * event the drag and surface detectors read.
40383
+ * - `"mousedown"` for the modal dialog, whose native `::backdrop` exists to
40384
+ * spend the press. The page's controls act on `mousedown`, and on a touch
40385
+ * screen that is not the same moment as `pointerdown`: the compatibility
40386
+ * mouse events are synthesized at `touchend` and hit-tested again then.
40387
+ * Closing on `pointerdown` takes the `::backdrop` away before that second
40388
+ * hit-test, so the `mousedown` lands on whatever the page holds under the
40389
+ * finger, and one tap both dismisses the dialog and presses the page. Read
40390
+ * on `mousedown`, the wall is still up when the press the page acts on is
40391
+ * aimed, and it catches it. Nothing behind a modal wall can cancel that
40392
+ * press, since nothing behind it hears the `pointerdown` at all.
40393
+ *
40394
+ * Capture phase, so a handler downstream that stops propagation cannot keep
40395
+ * the popup open either — the press is still outside whatever is made of it.
40396
+ *
40397
+ * Returns a cleanup that removes the listener.
40398
+ */
40399
+ const armOutsidePressClose = (
40400
+ popupEl,
40401
+ { openController, pressOutside, pressEventType = "pointerdown" },
40402
+ ) => {
40403
+ const onDocumentPointerDown = (pointerDownEvent) => {
40404
+ if (pointerDownEvent.button !== 0) {
40405
+ return;
40406
+ }
40407
+ if (openedDuringThisPress(openController)) {
40408
+ // The release of the press that opened it is not somebody dismissing it
40409
+ // (see openedDuringThisPress).
40410
+ return;
40411
+ }
40412
+ // The press landed inside another popup: that is a press on what is in
40413
+ // front, not outside. Asking the target where it lives rather than asking
40414
+ // this popup whether it was pushed — a popup in front does not have to be
40415
+ // one this popup knows about. In front means neither inside nor around: a
40416
+ // popup nested in this one falls to the containment check below as the
40417
+ // inside press it is, and one this popup opened INSIDE (a bubble rendered
40418
+ // in a sheet's content) is around it, so what that popup holds is outside
40419
+ // this one exactly like the rest of the page.
40420
+ const popupUnderPointer = pointerDownEvent.target.closest?.(
40421
+ `[navi-control="dialog"], [navi-control="popover"]`,
40422
+ );
40423
+ if (
40424
+ popupUnderPointer &&
40425
+ popupUnderPointer !== popupEl &&
40426
+ !popupEl.contains(popupUnderPointer) &&
40427
+ !popupUnderPointer.contains(popupEl)
40428
+ ) {
40429
+ return;
40430
+ }
40431
+ if (isDeclaredInsidePopup(pointerDownEvent.target, popupEl.id)) {
40432
+ return;
40433
+ }
40434
+ // Real DOM containment wins over the rectangle below — an element
40435
+ // genuinely inside the popup (`overflow: visible`, a negative margin, an
40436
+ // absolutely-positioned child) can be painted outside its border box, and
40437
+ // a press there is not outside just because its coordinates are. The
40438
+ // popup element itself is excluded (contains() answers true for it): a
40439
+ // press on a modal dialog's native backdrop reports the dialog as its
40440
+ // target, there being no `::backdrop` node to be one, so treating that as
40441
+ // contained would keep the rectangle from ever running.
40442
+ if (
40443
+ pointerDownEvent.target !== popupEl &&
40444
+ popupEl.contains(pointerDownEvent.target)
40445
+ ) {
40446
+ return;
40447
+ }
40448
+ const rect = popupEl.getBoundingClientRect();
40449
+ const isOutside =
40450
+ pointerDownEvent.clientX < rect.left ||
40451
+ pointerDownEvent.clientX > rect.right ||
40452
+ pointerDownEvent.clientY < rect.top ||
40453
+ pointerDownEvent.clientY > rect.bottom;
40454
+ if (!isOutside) {
40455
+ return;
40456
+ }
40457
+ openController.requestClose(pointerDownEvent, {
40458
+ isCancel: pressOutside === "cancel",
40459
+ });
40460
+ };
40461
+ document.addEventListener(pressEventType, onDocumentPointerDown, {
40462
+ capture: true,
40463
+ });
40464
+ return () => {
40465
+ document.removeEventListener(pressEventType, onDocumentPointerDown, {
40466
+ capture: true,
40467
+ });
40468
+ };
40469
+ };
40470
+
40447
40471
  /**
40448
40472
  * When a popup builds what it holds, and when it throws it away.
40449
40473
  *
@@ -49161,7 +49185,7 @@ const startNetworkMonitoring = () => {
49161
49185
  };
49162
49186
 
49163
49187
  installImportMetaCssBuild(import.meta);
49164
- const css$10 = /* css */`.navi_loading_indicator_fluid_container {
49188
+ const css$11 = /* css */`.navi_loading_indicator_fluid_container {
49165
49189
  border-radius: inherit;
49166
49190
  opacity: 1;
49167
49191
  width: 100%;
@@ -49192,7 +49216,7 @@ const LoadingIndicatorFluid = ({
49192
49216
  visuallyHidden,
49193
49217
  ...rest
49194
49218
  }) => {
49195
- import.meta.css = [css$10, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
49219
+ import.meta.css = [css$11, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
49196
49220
  const ref = useRef(null);
49197
49221
  // The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
49198
49222
  const [containerWidth, setContainerWidth] = useState(0);
@@ -49394,7 +49418,7 @@ const LoadingRectangleSvg = ({
49394
49418
  };
49395
49419
 
49396
49420
  installImportMetaCssBuild(import.meta);
49397
- const css$$ = /* css */`.navi_loading_outline_wrapper {
49421
+ const css$10 = /* css */`.navi_loading_outline_wrapper {
49398
49422
  top: max(var(--loading-outline-min-inset, -100vh),
49399
49423
  var(--loading-rectangle-top, 0px));
49400
49424
  right: max(var(--loading-outline-min-inset, -100vh),
@@ -49415,7 +49439,7 @@ const css$$ = /* css */`.navi_loading_outline_wrapper {
49415
49439
  }
49416
49440
  `;
49417
49441
  const LoadingOutline = props => {
49418
- import.meta.css = [css$$, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
49442
+ import.meta.css = [css$10, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
49419
49443
  if (props.containerRef) {
49420
49444
  const container = props.containerRef.current;
49421
49445
  if (!container) {
@@ -49617,7 +49641,7 @@ const flush = () => {
49617
49641
  };
49618
49642
 
49619
49643
  installImportMetaCssBuild(import.meta);
49620
- const css$_ = /* css */`.navi_text_anchor {
49644
+ const css$$ = /* css */`.navi_text_anchor {
49621
49645
  vertical-align: baseline;
49622
49646
  user-select: none;
49623
49647
  overflow: hidden;
@@ -49655,7 +49679,7 @@ const TextAnchor = ({
49655
49679
  textSize,
49656
49680
  lineLayout
49657
49681
  }) => {
49658
- import.meta.css = [css$_, "@jsenv/navi/src/text/text_anchor.jsx"];
49682
+ import.meta.css = [css$$, "@jsenv/navi/src/text/text_anchor.jsx"];
49659
49683
  const anchorRef = useRef();
49660
49684
  // The correction reads the layout, with the other reads of the commit (see
49661
49685
  // layout_batch.js) — an anchor per icon, each measured between two writes,
@@ -49906,7 +49930,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
49906
49930
  };
49907
49931
 
49908
49932
  installImportMetaCssBuild(import.meta);
49909
- const css$Z = /* css */`@layer navi {
49933
+ const css$_ = /* css */`@layer navi {
49910
49934
  .navi_text {
49911
49935
  &[data-skeleton] {
49912
49936
  border-radius: .2em;
@@ -50498,7 +50522,7 @@ const TextShrinkWrap = props => {
50498
50522
  });
50499
50523
  };
50500
50524
  const TextUI = props => {
50501
- import.meta.css = [css$Z, "@jsenv/navi/src/text/text.jsx"];
50525
+ import.meta.css = [css$_, "@jsenv/navi/src/text/text.jsx"];
50502
50526
  let {
50503
50527
  ref,
50504
50528
  spacing,
@@ -50736,7 +50760,7 @@ const Icon = ({
50736
50760
  fillLine,
50737
50761
  ...props
50738
50762
  }) => {
50739
- import.meta.css = [css$Z, "@jsenv/navi/src/text/text.jsx"];
50763
+ import.meta.css = [css$_, "@jsenv/navi/src/text/text.jsx"];
50740
50764
  const innerChildren = href ? jsx("svg", {
50741
50765
  width: "100%",
50742
50766
  height: "100%",
@@ -51070,7 +51094,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
51070
51094
  };
51071
51095
 
51072
51096
  installImportMetaCssBuild(import.meta);
51073
- const css$Y = /* css */`@layer navi {
51097
+ const css$Z = /* css */`@layer navi {
51074
51098
  .navi_link {
51075
51099
  --link-border-radius: unset;
51076
51100
  --link-outline-color: var(--navi-focus-outline-color);
@@ -51544,7 +51568,7 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
51544
51568
  * @param {boolean} [props.readOnly]
51545
51569
  */
51546
51570
  const Link = props => {
51547
- import.meta.css = [css$Y, "@jsenv/navi/src/nav/link/link.jsx"];
51571
+ import.meta.css = [css$Z, "@jsenv/navi/src/nav/link/link.jsx"];
51548
51572
  if (props.route) {
51549
51573
  return jsx(LinkWithRoute, {
51550
51574
  ...props
@@ -52104,7 +52128,7 @@ const rememberProgressAnimation = (navElement, travel) => {
52104
52128
  travel.progressAnimation = animation;
52105
52129
  travel.progressFrom = animation ? parseFloat(animation.effect.getKeyframes()[0]["--slide-travel-progress"]) || 0 : 0;
52106
52130
  };
52107
- const css$X = /* css */`@layer navi {
52131
+ const css$Y = /* css */`@layer navi {
52108
52132
  .navi_nav {
52109
52133
  --nav-border: none;
52110
52134
  --nav-padding: 0px;
@@ -52373,7 +52397,7 @@ const Nav = ({
52373
52397
  slideContainer,
52374
52398
  ...props
52375
52399
  }) => {
52376
- import.meta.css = [css$X, "@jsenv/navi/src/nav/link/nav.jsx"];
52400
+ import.meta.css = [css$Y, "@jsenv/navi/src/nav/link/nav.jsx"];
52377
52401
  const defaultRef = useRef();
52378
52402
  props.ref = props.ref || defaultRef;
52379
52403
  const navRef = props.ref;
@@ -53249,7 +53273,7 @@ const buildBinderCenterlinePathBelow = ({
53249
53273
  };
53250
53274
 
53251
53275
  installImportMetaCssBuild(import.meta);
53252
- const css$W = /* css */`@layer navi {
53276
+ const css$X = /* css */`@layer navi {
53253
53277
  .navi_binder {
53254
53278
  --binder-border-width: var(--navi-control-border-width);
53255
53279
  --binder-border-radius: var(--navi-control-border-radius);
@@ -53536,7 +53560,7 @@ const Binder = ({
53536
53560
  pagePadding,
53537
53561
  ...props
53538
53562
  }) => {
53539
- import.meta.css = [css$W, "@jsenv/navi/src/nav/binder/binder.jsx"];
53563
+ import.meta.css = [css$X, "@jsenv/navi/src/nav/binder/binder.jsx"];
53540
53564
  const items = toChildArray(children).map((child, index) => {
53541
53565
  const {
53542
53566
  value: itemValue,
@@ -53986,7 +54010,7 @@ const writeSpace = (area) => {
53986
54010
  };
53987
54011
 
53988
54012
  installImportMetaCssBuild(import.meta);
53989
- const css$V = /* css */`@layer navi {
54013
+ const css$W = /* css */`@layer navi {
53990
54014
  :root {
53991
54015
  --navi-fixed-bar-width: 56px;
53992
54016
  --navi-fixed-bar-height: 56px;
@@ -54109,7 +54133,7 @@ const FixedBar = ({
54109
54133
  border = true,
54110
54134
  ...props
54111
54135
  }) => {
54112
- import.meta.css = [css$V, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
54136
+ import.meta.css = [css$W, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
54113
54137
  const defaultRef = useRef();
54114
54138
  props.ref = props.ref || defaultRef;
54115
54139
  // Said with the width the border rule reads rather than with an attribute of
@@ -54203,7 +54227,7 @@ const FixedBar = ({
54203
54227
  // Subpixel layout rounds rectangles up on boxes that fit exactly.
54204
54228
  const OVERFLOW_TOLERANCE = 1;
54205
54229
 
54206
- const css$U = /* css */ `
54230
+ const css$V = /* css */ `
54207
54231
  [data-navi-overflow-x] {
54208
54232
  outline: 2px dashed #e74c3c;
54209
54233
  outline-offset: -2px;
@@ -54227,7 +54251,7 @@ const detectHorizontalOverflow = ({
54227
54251
  let styleEl = null;
54228
54252
  if (highlight) {
54229
54253
  styleEl = document.createElement("style");
54230
- styleEl.textContent = css$U;
54254
+ styleEl.textContent = css$V;
54231
54255
  document.head.appendChild(styleEl);
54232
54256
  }
54233
54257
 
@@ -54317,7 +54341,11 @@ const findOverflows = (root) => {
54317
54341
  if (style.display === "none" || style.position === "fixed") {
54318
54342
  continue;
54319
54343
  }
54320
- if (el.hasAttribute("popover") || el.tagName === "DIALOG") {
54344
+ if (
54345
+ el.hasAttribute("popover") ||
54346
+ // An idle PopupGroup is a box of the page (see popup_group.jsx).
54347
+ (el.tagName === "DIALOG" && !el.hasAttribute("navi-popup-group"))
54348
+ ) {
54321
54349
  continue;
54322
54350
  }
54323
54351
  const rect = el.getBoundingClientRect();
@@ -54383,7 +54411,7 @@ const useFocusGroup = (
54383
54411
 
54384
54412
  installImportMetaCssBuild(import.meta);
54385
54413
  const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
54386
- const css$T = /* css */`.navi_summary_marker {
54414
+ const css$U = /* css */`.navi_summary_marker {
54387
54415
  flex-shrink: 0;
54388
54416
  width: 1em;
54389
54417
  height: 1em;
@@ -54475,7 +54503,7 @@ const SummaryMarker = ({
54475
54503
  loading,
54476
54504
  openDirection = "down"
54477
54505
  }) => {
54478
- import.meta.css = [css$T, "@jsenv/navi/src/control/details/summary_marker.jsx"];
54506
+ import.meta.css = [css$U, "@jsenv/navi/src/control/details/summary_marker.jsx"];
54479
54507
  const showLoading = useDebounceTrue(loading, 300);
54480
54508
  return jsx("span", {
54481
54509
  className: "navi_summary_marker",
@@ -54521,7 +54549,7 @@ const SummaryMarker = ({
54521
54549
  };
54522
54550
 
54523
54551
  installImportMetaCssBuild(import.meta);
54524
- const css$S = /* css */`.navi_details {
54552
+ const css$T = /* css */`.navi_details {
54525
54553
  z-index: 1;
54526
54554
  flex-direction: column;
54527
54555
  flex-shrink: 0;
@@ -54566,7 +54594,7 @@ const Details = props => {
54566
54594
  return details;
54567
54595
  };
54568
54596
  const DetailsField = props => {
54569
- import.meta.css = [css$S, "@jsenv/navi/src/control/details/details.jsx"];
54597
+ import.meta.css = [css$T, "@jsenv/navi/src/control/details/details.jsx"];
54570
54598
  const {
54571
54599
  ref,
54572
54600
  persists,
@@ -54732,7 +54760,7 @@ const DetailsFieldContent = ({
54732
54760
  };
54733
54761
 
54734
54762
  installImportMetaCssBuild(import.meta);
54735
- const css$R = /* css */`.navi_expandable {
54763
+ const css$S = /* css */`.navi_expandable {
54736
54764
  flex-direction: column;
54737
54765
  flex-shrink: 0;
54738
54766
  display: flex;
@@ -54976,7 +55004,7 @@ const useExpandableContext = partName => {
54976
55004
  * around them (docs/control_object.md, "A control that is not there").
54977
55005
  */
54978
55006
  const Expandable = props => {
54979
- import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
55007
+ import.meta.css = [css$S, "@jsenv/navi/src/control/expandable/expandable.jsx"];
54980
55008
  /* The open props are read from `props` by the open controller below; they
54981
55009
  are named here only to keep them out of `rest`, and so out of the DOM. */
54982
55010
  /* eslint-disable no-unused-vars */
@@ -55957,7 +55985,7 @@ const useAccentColorAttributes = (
55957
55985
  };
55958
55986
 
55959
55987
  installImportMetaCssBuild(import.meta);
55960
- const css$Q = /* css */`@layer navi {
55988
+ const css$R = /* css */`@layer navi {
55961
55989
  .navi_button {
55962
55990
  --button-border-radius: var(--navi-control-border-radius);
55963
55991
  --button-border-width: var(--navi-control-border-width);
@@ -56261,7 +56289,7 @@ a.navi_button {
56261
56289
  }
56262
56290
  `;
56263
56291
  const ButtonUI = props => {
56264
- import.meta.css = [css$Q, "@jsenv/navi/src/control/input/button_ui.jsx"];
56292
+ import.meta.css = [css$R, "@jsenv/navi/src/control/input/button_ui.jsx"];
56265
56293
  const {
56266
56294
  ref,
56267
56295
  // href/link
@@ -56476,7 +56504,7 @@ const ButtonShadow = () => {
56476
56504
  markAsOutsideTextFlow(ButtonShadow);
56477
56505
 
56478
56506
  installImportMetaCssBuild(import.meta);
56479
- const css$P = /* css */`.navi_button_confirm {
56507
+ const css$Q = /* css */`.navi_button_confirm {
56480
56508
  align-items: center;
56481
56509
  gap: var(--navi-s);
56482
56510
  flex-wrap: wrap;
@@ -56504,7 +56532,7 @@ const ButtonConfirm = ({
56504
56532
  cancelTestId,
56505
56533
  ...props
56506
56534
  }) => {
56507
- import.meta.css = [css$P, "@jsenv/navi/src/control/input/button_confirm.jsx"];
56535
+ import.meta.css = [css$Q, "@jsenv/navi/src/control/input/button_confirm.jsx"];
56508
56536
  const [asking, setAsking] = useState(false);
56509
56537
  const askingRef = useRef(null);
56510
56538
  const cancelRef = useRef(null);
@@ -56965,7 +56993,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
56965
56993
  const Button = /*#__PURE__*/createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonConfirmResolver, ButtonHrefResolver, ButtonUI]);
56966
56994
 
56967
56995
  installImportMetaCssBuild(import.meta);
56968
- const css$O = /* css */`.navi_control_swap {
56996
+ const css$P = /* css */`.navi_control_swap {
56969
56997
  font-size: var(--navi-control-font-size);
56970
56998
  --x-control-swap-size: var(--navi-control-swap-size, calc(var(--navi-control-line-height) + 2 * var(--navi-s) + 2 *
56971
56999
  var(--navi-control-border-width)));
@@ -57070,7 +57098,7 @@ const css$O = /* css */`.navi_control_swap {
57070
57098
  * is the space between the boxes.
57071
57099
  */
57072
57100
  const ControlSwap = props => {
57073
- import.meta.css = [css$O, "@jsenv/navi/src/control/control_swap/control_swap.jsx"];
57101
+ import.meta.css = [css$P, "@jsenv/navi/src/control/control_swap/control_swap.jsx"];
57074
57102
  const {
57075
57103
  value,
57076
57104
  defaultValue,
@@ -57446,7 +57474,7 @@ const ControlGroup = props => {
57446
57474
  const CONTROL_GROUP_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
57447
57475
 
57448
57476
  installImportMetaCssBuild(import.meta);
57449
- const css$N = /* css */`@layer navi {
57477
+ const css$O = /* css */`@layer navi {
57450
57478
  .navi_checkbox {
57451
57479
  --switch-margin: 0;
57452
57480
  --switch-padding: 2px;
@@ -57503,7 +57531,7 @@ const css$N = /* css */`@layer navi {
57503
57531
  }
57504
57532
  `;
57505
57533
  const SwitchUI = () => {
57506
- import.meta.css = [css$N, "@jsenv/navi/src/control/input/switch_ui.jsx"];
57534
+ import.meta.css = [css$O, "@jsenv/navi/src/control/input/switch_ui.jsx"];
57507
57535
  return jsx(Box, {
57508
57536
  className: "navi_switch",
57509
57537
  as: "svg",
@@ -57546,7 +57574,7 @@ const useCheckableProps = (props, options) => {
57546
57574
  };
57547
57575
 
57548
57576
  installImportMetaCssBuild(import.meta);
57549
- const css$M = /* css */`@layer navi {
57577
+ const css$N = /* css */`@layer navi {
57550
57578
  .navi_checkbox {
57551
57579
  --border-radius: var(--navi-checkbox-border-radius);
57552
57580
  --border-width: var(--navi-control-border-width);
@@ -57819,7 +57847,7 @@ const InputCheckboxHeadless = props => {
57819
57847
  });
57820
57848
  };
57821
57849
  const InputCheckboxFieldInterface = props => {
57822
- import.meta.css = [css$M, "@jsenv/navi/src/control/input/input_checkbox.jsx"];
57850
+ import.meta.css = [css$N, "@jsenv/navi/src/control/input/input_checkbox.jsx"];
57823
57851
  const [checkboxRootProps, checkboxHostProps] = useCheckableProps(props);
57824
57852
  const {
57825
57853
  icon,
@@ -57941,7 +57969,7 @@ const CheckboxPseudoClasses = [":hover", ":active", ":focus", ":focus-visible",
57941
57969
  const CheckboxPseudoElements = ["::-navi-loader", "::-navi-checkmark"];
57942
57970
 
57943
57971
  installImportMetaCssBuild(import.meta);
57944
- const css$L = /* css */`@layer navi {
57972
+ const css$M = /* css */`@layer navi {
57945
57973
  .navi_label {
57946
57974
  --label-required-indicator-color: var(--navi-color-danger, #b42318);
57947
57975
 
@@ -58019,7 +58047,7 @@ const css$L = /* css */`@layer navi {
58019
58047
  * </Field>
58020
58048
  */
58021
58049
  const Field = props => {
58022
- import.meta.css = [css$L, "@jsenv/navi/src/control/field.jsx"];
58050
+ import.meta.css = [css$M, "@jsenv/navi/src/control/field.jsx"];
58023
58051
  const refDefault = useRef();
58024
58052
  props.ref = props.ref || refDefault;
58025
58053
  const {
@@ -58054,7 +58082,7 @@ const FieldCSSVars = {
58054
58082
  spacingWithControl: ["--spacing-with-control", "padding"]
58055
58083
  };
58056
58084
  const FieldAsContainer = props => {
58057
- import.meta.css = [css$L, "@jsenv/navi/src/control/field.jsx"];
58085
+ import.meta.css = [css$M, "@jsenv/navi/src/control/field.jsx"];
58058
58086
  const {
58059
58087
  children
58060
58088
  } = props;
@@ -58085,7 +58113,7 @@ const FieldAsContainer = props => {
58085
58113
  };
58086
58114
  const FIELD_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
58087
58115
  const Label = props => {
58088
- import.meta.css = [css$L, "@jsenv/navi/src/control/field.jsx"];
58116
+ import.meta.css = [css$M, "@jsenv/navi/src/control/field.jsx"];
58089
58117
  const {
58090
58118
  children,
58091
58119
  // Marks the label when its control is required. Takes what to show, or
@@ -58253,7 +58281,7 @@ const InputSlot = ({
58253
58281
  };
58254
58282
 
58255
58283
  installImportMetaCssBuild(import.meta);
58256
- const css$K = /* css */`@layer navi {
58284
+ const css$L = /* css */`@layer navi {
58257
58285
  .navi_radio {
58258
58286
  --margin: 3px 3px 3px 5px;
58259
58287
  --outline-offset: 1px;
@@ -58559,7 +58587,7 @@ const InputRadioHeadless = props => {
58559
58587
  };
58560
58588
  const VARIANT_SET = new Set(["icon", "button", "radio"]);
58561
58589
  const InputRadioFieldInterface = props => {
58562
- import.meta.css = [css$K, "@jsenv/navi/src/control/input/input_radio.jsx"];
58590
+ import.meta.css = [css$L, "@jsenv/navi/src/control/input/input_radio.jsx"];
58563
58591
  const [radioRootProps, radioHostProps] = useCheckableProps(props);
58564
58592
  const {
58565
58593
  icon,
@@ -58705,7 +58733,7 @@ const RadioPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":r
58705
58733
  const RadioPseudoElements = ["::-navi-loader", "::-navi-radiomark"];
58706
58734
 
58707
58735
  installImportMetaCssBuild(import.meta);
58708
- const css$J = /* css */`@layer navi {
58736
+ const css$K = /* css */`@layer navi {
58709
58737
  .navi_input_range {
58710
58738
  --border-radius: 6px;
58711
58739
  --border-width: var(--navi-control-border-width);
@@ -58944,7 +58972,7 @@ const InputRange = props => {
58944
58972
  });
58945
58973
  };
58946
58974
  const InputRangeFieldInterface = props => {
58947
- import.meta.css = [css$J, "@jsenv/navi/src/control/input/input_range.jsx"];
58975
+ import.meta.css = [css$K, "@jsenv/navi/src/control/input/input_range.jsx"];
58948
58976
  const {
58949
58977
  ref
58950
58978
  } = props;
@@ -60453,7 +60481,7 @@ Input.UI = {
60453
60481
  };
60454
60482
 
60455
60483
  installImportMetaCssBuild(import.meta);
60456
- const css$I = /* css */`.navi_textarea {
60484
+ const css$J = /* css */`.navi_textarea {
60457
60485
  & textarea {
60458
60486
  min-height: calc(var(--textarea-min-rows, 1.5) * 1lh);
60459
60487
  max-height: calc(var(--textarea-max-rows, 99999) * 1lh);
@@ -60540,7 +60568,7 @@ const Textarea = ({
60540
60568
  ...props
60541
60569
  }) => {
60542
60570
  installInputCss();
60543
- import.meta.css = [css$I, "@jsenv/navi/src/control/input/textarea.jsx"];
60571
+ import.meta.css = [css$J, "@jsenv/navi/src/control/input/textarea.jsx"];
60544
60572
  const defaultRef = useRef(null);
60545
60573
  props.ref = props.ref || defaultRef;
60546
60574
  usePlaceholderHeight(props.ref, props.placeholder);
@@ -60622,7 +60650,7 @@ const TextareaCharCount = ({
60622
60650
  maxLength,
60623
60651
  ...rest
60624
60652
  }) => {
60625
- import.meta.css = [css$I, "@jsenv/navi/src/control/input/textarea.jsx"];
60653
+ import.meta.css = [css$J, "@jsenv/navi/src/control/input/textarea.jsx"];
60626
60654
  const resolvedValue = signal ? signal.value : value;
60627
60655
  const length = typeof resolvedValue === "string" ? resolvedValue.length : 0;
60628
60656
  return jsx(Box, {
@@ -60732,7 +60760,7 @@ const RealTextarea = ({
60732
60760
  };
60733
60761
 
60734
60762
  installImportMetaCssBuild(import.meta);
60735
- const css$H = /* css */`.navi_editable_wrapper {
60763
+ const css$I = /* css */`.navi_editable_wrapper {
60736
60764
  --inset-top: 0px;
60737
60765
  --inset-right: 0px;
60738
60766
  --inset-bottom: 0px;
@@ -60902,7 +60930,7 @@ const useEditionController = () => {
60902
60930
  * Every other unknown prop goes to the field itself, styling props included.
60903
60931
  */
60904
60932
  const Editable = props => {
60905
- import.meta.css = [css$H, "@jsenv/navi/src/control/edition/editable.jsx"];
60933
+ import.meta.css = [css$I, "@jsenv/navi/src/control/edition/editable.jsx"];
60906
60934
  let {
60907
60935
  children,
60908
60936
  action,
@@ -61513,7 +61541,7 @@ const useRequestSubmitThroughGates = ref => {
61513
61541
  // };
61514
61542
 
61515
61543
  installImportMetaCssBuild(import.meta);
61516
- const css$G = /* css */`.navi_group {
61544
+ const css$H = /* css */`.navi_group {
61517
61545
  --group-border-width: var(--navi-control-border-width);
61518
61546
 
61519
61547
  & > :not([navi-out-of-flow]) {
@@ -61574,7 +61602,7 @@ const Group = ({
61574
61602
  vertical = row,
61575
61603
  ...props
61576
61604
  }) => {
61577
- import.meta.css = [css$G, "@jsenv/navi/src/control/group.jsx"];
61605
+ import.meta.css = [css$H, "@jsenv/navi/src/control/group.jsx"];
61578
61606
  return jsx(Box, {
61579
61607
  baseClassName: "navi_group",
61580
61608
  "data-vertical": vertical ? "" : undefined
@@ -61706,7 +61734,7 @@ const unfreezeSize = (el) => {
61706
61734
  };
61707
61735
 
61708
61736
  installImportMetaCssBuild(import.meta);
61709
- const css$F = /* css */`@property --slide-travel-progress {
61737
+ const css$G = /* css */`@property --slide-travel-progress {
61710
61738
  syntax: "<number>";
61711
61739
  inherits: true;
61712
61740
  initial-value: 0;
@@ -62193,7 +62221,7 @@ const SlideContainer = ({
62193
62221
  children,
62194
62222
  ...rest
62195
62223
  }) => {
62196
- import.meta.css = [css$F, "@jsenv/navi/src/layout/slide_container.jsx"];
62224
+ import.meta.css = [css$G, "@jsenv/navi/src/layout/slide_container.jsx"];
62197
62225
  const debugFocus = useDebugFocus();
62198
62226
  const trackRef = useRef();
62199
62227
  // The box itself: it is what takes the keyboard when what is on screen holds
@@ -65544,6 +65572,1269 @@ const renderSafe = (value) => {
65544
65572
  return String(value);
65545
65573
  };
65546
65574
 
65575
+ installImportMetaCssBuild(import.meta);
65576
+ const PopupGroupContext = createContext(null);
65577
+
65578
+ /**
65579
+ * What a press outside a popup of the group does, read against a wall that is
65580
+ * always up: "ignore" closes, as "close" does. For a press the group does not
65581
+ * see itself, on a region the popup declared outside
65582
+ * (handlePressOnOutsideRegion).
65583
+ */
65584
+ const readPressOutsideInPopupGroup = pressOutside => pressOutside === "ignore" ? "close" : pressOutside;
65585
+
65586
+ // The groups shown modal right now, for the popup around one of them to close
65587
+ // it first (see closePopupGroupsInside).
65588
+ const engagedGroupSet = new Set();
65589
+
65590
+ /**
65591
+ * Takes the groups engaged inside `popupElement` down, with what they have
65592
+ * open, before that popup gives the focus back: while a group is the topmost
65593
+ * modal everything else is inert, the element the focus goes back to
65594
+ * included. Called by Dialog and Popover as they close.
65595
+ */
65596
+ const closePopupGroupsInside = (popupElement, closeEvent) => {
65597
+ for (const group of engagedGroupSet) {
65598
+ if (popupElement.contains(group.getElement())) {
65599
+ group.closeEverything(closeEvent);
65600
+ }
65601
+ }
65602
+ };
65603
+ const css$F = /* css */`@property --x-popup-group-top {
65604
+ syntax: "*";
65605
+ inherits: false
65606
+ }
65607
+
65608
+ @property --x-popup-group-left {
65609
+ syntax: "*";
65610
+ inherits: false
65611
+ }
65612
+
65613
+ @property --x-popup-group-width {
65614
+ syntax: "*";
65615
+ inherits: false
65616
+ }
65617
+
65618
+ @property --x-popup-group-height {
65619
+ syntax: "*";
65620
+ inherits: false
65621
+ }
65622
+
65623
+ @property --x-popup-group-padding {
65624
+ syntax: "*";
65625
+ inherits: false
65626
+ }
65627
+
65628
+ @property --x-popup-group-clip {
65629
+ syntax: "*";
65630
+ inherits: false
65631
+ }
65632
+
65633
+ @layer navi {
65634
+ .navi_popup_group {
65635
+ --backdrop-background: var(--navi-backdrop-close-background);
65636
+ --backdrop-filter: var(--navi-backdrop-close-backdrop-filter);
65637
+ width: auto;
65638
+ max-width: none;
65639
+ height: auto;
65640
+ max-height: none;
65641
+ color: inherit;
65642
+ background: none;
65643
+ border: none;
65644
+ margin: 0;
65645
+ padding: 0;
65646
+ display: flow-root;
65647
+ position: static;
65648
+ inset: auto;
65649
+ overflow: visible;
65650
+ }
65651
+ }
65652
+
65653
+ .navi_popup_group:modal {
65654
+ inset: auto !important;
65655
+ top: var(--x-popup-group-top) !important;
65656
+ left: var(--x-popup-group-left) !important;
65657
+ box-sizing: border-box !important;
65658
+ width: var(--x-popup-group-width) !important;
65659
+ min-width: 0 !important;
65660
+ max-width: none !important;
65661
+ height: var(--x-popup-group-height) !important;
65662
+ min-height: 0 !important;
65663
+ max-height: none !important;
65664
+ padding: var(--x-popup-group-padding) !important;
65665
+ clip-path: var(--x-popup-group-clip, none) !important;
65666
+ margin: 0 !important;
65667
+ position: fixed !important;
65668
+ }
65669
+
65670
+ .navi_popup_group[data-backdrop-variant="lift"] {
65671
+ --backdrop-background: var(--navi-backdrop-lift-background);
65672
+ --backdrop-filter: var(--navi-backdrop-lift-backdrop-filter);
65673
+ }
65674
+
65675
+ .navi_popup_group[data-backdrop-variant="discrete"] {
65676
+ --backdrop-background: var(--navi-backdrop-discrete-background);
65677
+ --backdrop-filter: var(--navi-backdrop-discrete-backdrop-filter);
65678
+ }
65679
+
65680
+ .navi_popup_group[data-backdrop-variant="invisible"] {
65681
+ --backdrop-background: transparent;
65682
+ --backdrop-filter: none;
65683
+ }
65684
+
65685
+ .navi_popup_group::backdrop {
65686
+ background: var(--backdrop-background);
65687
+ opacity: 1;
65688
+ backdrop-filter: var(--backdrop-filter);
65689
+ transition: opacity var(--popup-animation-duration, .18s) ease;
65690
+
65691
+ @starting-style {
65692
+ opacity: 0;
65693
+ }
65694
+ }
65695
+
65696
+ .navi_popup_group[navi-transitions-off]::backdrop {
65697
+ transition: none;
65698
+ }
65699
+
65700
+ :is(:root[data-navi-route-transition] .navi_popup_group:modal, :root[data-navi-route-travel] .navi_popup_group:modal) {
65701
+ view-transition-class: navi_popup;
65702
+ }
65703
+
65704
+ @supports (view-transition-name: match-element) {
65705
+ :root[data-navi-route-transition]:not([data-navi-route-transition-target="area"]) .navi_popup_group:modal {
65706
+ view-transition-name: match-element;
65707
+ }
65708
+
65709
+ :root[data-navi-route-travel] .navi_popup_group:modal {
65710
+ view-transition-name: match-element;
65711
+ }
65712
+ }
65713
+ `;
65714
+
65715
+ /**
65716
+ * The box around popups that hand over to each other: score cells, a menu bar,
65717
+ * a toolbar. Being inside it is what makes a popup and a button peers: no name
65718
+ * to give.
65719
+ *
65720
+ * While one of its popups is open, the group is shown modal where it stands:
65721
+ * what it holds stays lit and live, the rest of the screen is behind its wall,
65722
+ * and a press on another of its popups' triggers closes the open one and opens
65723
+ * that one in the same press. Its popups draw no wall of their own (their
65724
+ * backdrop props are not read), and a Dialog in it is never modal. The page
65725
+ * behind stays still while one of its Popovers or Dialogs is open; a callout
65726
+ * of it lets the page scroll, as a callout does anywhere.
65727
+ *
65728
+ * Its element is a `<dialog>`, closed at rest and laid out like any box: the
65729
+ * app styles it as it would a `<div>`. While engaged, an invisible placeholder
65730
+ * holds its place in the page, as the element before it, marked
65731
+ * `navi-out-of-flow` like everything navi adds to a tree. See
65732
+ * docs/popup_backdrop.md.
65733
+ *
65734
+ * @type {import("ignore:preact").FunctionComponent<{
65735
+ * backdropVariant?: "auto" | "lift" | "discrete" | "invisible",
65736
+ * backdropColor?: string,
65737
+ * backdropFilter?: string,
65738
+ * themeColor?: string | false,
65739
+ * role?: string,
65740
+ * children?: import("ignore:preact").ComponentChildren,
65741
+ * [key: string]: any,
65742
+ * }>}
65743
+ * @param {"auto"|"lift"|"discrete"|"invisible"} [props.backdropVariant="auto"] -
65744
+ * How visible the wall is: `"auto"` is the wash a popup closing on an
65745
+ * outside press paints, `"lift"` the opaque blurred wall, `"discrete"` a
65746
+ * barely-there dim, `"invisible"` a wall that is not seen and still takes the
65747
+ * press.
65748
+ * @param {string} [props.backdropColor] - The wash itself, over the tokens.
65749
+ * @param {string} [props.backdropFilter] - What that wash does to what is
65750
+ * behind it.
65751
+ * @param {string|false} [props.themeColor] - The browser's colour while the
65752
+ * wall is up (see docs/popup_backdrop.md): worked out from the wash by
65753
+ * default.
65754
+ * @param {string} [props.role="group"] - A box of the page, not a dialog: the
65755
+ * element is a `<dialog>` for its modality alone.
65756
+ */
65757
+ const PopupGroup = props => {
65758
+ import.meta.css = [css$F, "@jsenv/navi/src/layout/popup_group.jsx"];
65759
+ const {
65760
+ backdropVariant = "auto",
65761
+ backdropColor,
65762
+ backdropFilter,
65763
+ themeColor,
65764
+ role = "group",
65765
+ children,
65766
+ ...rest
65767
+ } = props;
65768
+ const group = useMemo(createPopupGroup, []);
65769
+ group.themeColor = themeColor;
65770
+ // Unmounted or parked while engaged: a modal left open with its tree gone
65771
+ // keeps the whole document inert.
65772
+ useLayoutEffect(() => {
65773
+ return () => {
65774
+ group.disengage({
65775
+ leavingTree: true
65776
+ });
65777
+ };
65778
+ }, [group]);
65779
+ return jsx(Box, {
65780
+ as: "dialog",
65781
+ ...rest,
65782
+ // A callback ignoring null: a Suspense park nulls an object ref and
65783
+ // gives it back only when its identity changes.
65784
+ ref: group.captureElement,
65785
+ baseClassName: "navi_popup_group",
65786
+ "navi-popup-group": "",
65787
+ role: role,
65788
+ "data-backdrop-variant": backdropVariant === "auto" ? undefined : backdropVariant,
65789
+ backdropColor: backdropColor,
65790
+ backdropFilter: backdropFilter,
65791
+ styleCSSVars: POPUP_GROUP_STYLE_CSS_VARS,
65792
+ children: jsx(PopupGroupContext.Provider, {
65793
+ value: group,
65794
+ children: children
65795
+ })
65796
+ });
65797
+ };
65798
+ const POPUP_GROUP_STYLE_CSS_VARS = {
65799
+ backdropColor: "--backdrop-background",
65800
+ backdropFilter: "--backdrop-filter"
65801
+ };
65802
+
65803
+ // How long after the release a press that closed a popup may still bring the
65804
+ // click that ends it: the compatibility events of a tap come after touchend,
65805
+ // in a task of their own.
65806
+ const END_OF_PRESS_SAFETY_MS = 1000;
65807
+ const createPopupGroup = () => {
65808
+ let groupEl = null;
65809
+ // Every popup rendered in the group, open or not: "is one of them open" is
65810
+ // their own decision, read before any of them shows (a lifting dialog shows a
65811
+ // frame after it decided).
65812
+ const memberControllerSet = new Set();
65813
+ // The popups of the group that are open, as their openEffect described
65814
+ // them, the last one to open last. One at a time, except when the open one
65815
+ // refuses to close as another opens (a value it cannot note): it is kept
65816
+ // here, so the group can still close it.
65817
+ const openMemberMap = new Map();
65818
+ let engagement = null;
65819
+ const noMemberOpen = () => {
65820
+ for (const openController of memberControllerSet) {
65821
+ if (openController.opened) {
65822
+ return false;
65823
+ }
65824
+ }
65825
+ return true;
65826
+ };
65827
+ const getOpenMembers = () => {
65828
+ const openMembers = [];
65829
+ for (const [openController, member] of openMemberMap) {
65830
+ if (openController.opened) {
65831
+ openMembers.push(member);
65832
+ } else {
65833
+ openMemberMap.delete(openController);
65834
+ }
65835
+ }
65836
+ return openMembers;
65837
+ };
65838
+ // The page behind is held still unless what is open is a callout, which
65839
+ // lets it scroll as it does anywhere (its closeByScroll answers that
65840
+ // scroll). Kept as it is while nothing is open: the wall stays up until the
65841
+ // press that closed the last popup ends.
65842
+ const followScrollLock = () => {
65843
+ const openMembers = getOpenMembers();
65844
+ if (openMembers.length === 0) {
65845
+ return;
65846
+ }
65847
+ engagement.followScrollLock(openMembers.some(openMember => !openMember.pageScrolls));
65848
+ };
65849
+ const group = {
65850
+ themeColor: undefined,
65851
+ captureElement: element => {
65852
+ if (element) {
65853
+ groupEl = element;
65854
+ }
65855
+ },
65856
+ getElement: () => groupEl,
65857
+ register: openController => {
65858
+ memberControllerSet.add(openController);
65859
+ return () => {
65860
+ memberControllerSet.delete(openController);
65861
+ };
65862
+ },
65863
+ /**
65864
+ * Called by a popup of the group as it opens, before it shows itself.
65865
+ * `member`: { openController, popupElement, pressOutside, event,
65866
+ * pageScrolls, popupId }. `popupElement` is read at each press, so a
65867
+ * member whose element is drawn after it engaged (a callout) passes a
65868
+ * getter. `pageScrolls`: the page behind may scroll while it is open.
65869
+ * `popupId`: the id a data-navi-popup-inside names it by, when it is not
65870
+ * the popup element's own.
65871
+ */
65872
+ engage: member => {
65873
+ // One popup of the group at a time. A press on a peer has closed the
65874
+ // open one already (see armPressArbiter): this one opened from the
65875
+ // keyboard or from code.
65876
+ for (const openMember of getOpenMembers()) {
65877
+ if (openMember.openController === member.openController) {
65878
+ continue;
65879
+ }
65880
+ if (openMember.openController.requestClose(member.event)) {
65881
+ openMemberMap.delete(openMember.openController);
65882
+ }
65883
+ }
65884
+ openMemberMap.delete(member.openController);
65885
+ openMemberMap.set(member.openController, member);
65886
+ if (engagement) {
65887
+ engagement.cancelPendingCheck();
65888
+ } else {
65889
+ if (!groupEl || !groupEl.isConnected) {
65890
+ return;
65891
+ }
65892
+ engagement = engageGroup(groupEl, group, {
65893
+ silent: Boolean(member.event?.detail?.silent)
65894
+ });
65895
+ engagedGroupSet.add(group);
65896
+ }
65897
+ followScrollLock();
65898
+ },
65899
+ /**
65900
+ * Called by a popup of the group as it closes. Whether the group goes back
65901
+ * into the page is decided once nothing of the group is open — at the end
65902
+ * of the press that closed it when a press did, since that same press may
65903
+ * be opening a peer.
65904
+ */
65905
+ release: (openController, closeEvent) => {
65906
+ openMemberMap.delete(openController);
65907
+ if (!engagement) {
65908
+ return;
65909
+ }
65910
+ followScrollLock();
65911
+ if (findEvent(closeEvent, ["pointerdown", "mousedown", "pointerup", "mouseup", "click"])) {
65912
+ if (findEvent(closeEvent, "click") || !engagement.pressInFlight) {
65913
+ // Closed from inside the click, or after the press ended (a lifting
65914
+ // Dialog closes in a view transition's update callback, a frame
65915
+ // later): what is left of the press is over once this task is.
65916
+ engagement.checkAfterTask();
65917
+ return;
65918
+ }
65919
+ engagement.pressClosed = true;
65920
+ return;
65921
+ }
65922
+ // Not a press (Escape, a command, code): a peer opening in the same
65923
+ // stack, from the keyboard, is the only thing to wait for.
65924
+ engagement.checkAfterMicrotask();
65925
+ },
65926
+ getOpenMember: () => {
65927
+ const openMembers = getOpenMembers();
65928
+ return openMembers.length ? openMembers[openMembers.length - 1] : null;
65929
+ },
65930
+ disengageIfNothingOpen: () => {
65931
+ if (engagement && noMemberOpen()) {
65932
+ group.disengage();
65933
+ }
65934
+ },
65935
+ // Nothing of the group may stay open over a page that is live again.
65936
+ closeEverything: closeEvent => {
65937
+ for (const openMember of getOpenMembers()) {
65938
+ openMember.openController.close(closeEvent);
65939
+ }
65940
+ openMemberMap.clear();
65941
+ group.disengage();
65942
+ },
65943
+ disengage: ({
65944
+ leavingTree = false
65945
+ } = {}) => {
65946
+ if (!engagement) {
65947
+ return;
65948
+ }
65949
+ const leavingEngagement = engagement;
65950
+ engagement = null;
65951
+ engagedGroupSet.delete(group);
65952
+ leavingEngagement.cleanup({
65953
+ leavingTree
65954
+ });
65955
+ }
65956
+ };
65957
+ return group;
65958
+ };
65959
+ const engageGroup = (groupEl, group, {
65960
+ silent
65961
+ }) => {
65962
+ // Everything read before anything is written.
65963
+ const rect = groupEl.getBoundingClientRect();
65964
+ const groupStyle = getComputedStyle(groupEl);
65965
+ // Copied out: the style is live, and shown modal the group has its margins
65966
+ // and its bounds forced.
65967
+ const {
65968
+ marginLeft,
65969
+ marginRight,
65970
+ minWidth,
65971
+ maxWidth
65972
+ } = groupStyle;
65973
+ const ancestorClip = readAncestorClip(groupEl, groupStyle);
65974
+ const baselineDescent = readBaselineDescent(groupEl, groupStyle, rect);
65975
+ const themeColor = readPopupThemeColor(groupEl, group.themeColor);
65976
+ const focusedBefore = document.activeElement;
65977
+ const focusWasInGroup = Boolean(focusedBefore) && focusedBefore !== groupEl && groupEl.contains(focusedBefore);
65978
+ const ringBefore = Boolean(focusedBefore?.matches?.(":focus-visible"));
65979
+ const {
65980
+ scrollTop,
65981
+ scrollLeft
65982
+ } = groupEl;
65983
+ // A gesture on a box fixed in the top layer goes to the viewport: only a
65984
+ // place that scrolls with the viewport moves under it.
65985
+ const placeScroller = getScrollContainer(groupEl);
65986
+ const placeScrollsWithViewport = (placeScroller === document.scrollingElement ||
65987
+ // Reported as a scroll container on an app setting `body {
65988
+ // overflow-x: hidden }` (its computed overflow-y turns auto), it never
65989
+ // scrolls itself: the viewport does.
65990
+ placeScroller === document.body && bodyOverflowIsTheViewports()) && !findSelfOrAncestorFixedPosition(groupEl);
65991
+ const placeholder = createPlaceholder(groupStyle, rect, baselineDescent);
65992
+ groupEl.before(placeholder);
65993
+ groupEl.style.setProperty("--x-popup-group-padding", `${groupStyle.paddingTop} ${groupStyle.paddingRight} ${groupStyle.paddingBottom} ${groupStyle.paddingLeft}`);
65994
+ const visibleBox = writePlacement(groupEl, rect, ancestorClip);
65995
+ if (silent) {
65996
+ groupEl.setAttribute("navi-transitions-off", "");
65997
+ }
65998
+ // showModal() runs the dialog focusing steps: it focuses the first element
65999
+ // marked autofocus, or else the first focusable the group holds — and
66000
+ // scrolls the group's own scroller to it. Pointed at what already has the
66001
+ // focus, or at the group itself, with any other mark set aside for the
66002
+ // call, so nothing moves. A native autofocus is otherwise never used (see
66003
+ // the focus skill): this one exists for the length of the call, to steer
66004
+ // the browser's own pick, not to transfer anything.
66005
+ const autofocusTarget = focusWasInGroup ? focusedBefore : groupEl;
66006
+ const autofocusWasSet = autofocusTarget.hasAttribute("autofocus");
66007
+ const autofocusSetAside = focusWasInGroup ? [...groupEl.querySelectorAll("[autofocus]")].filter(element => element !== autofocusTarget) : [];
66008
+ for (const element of autofocusSetAside) {
66009
+ element.removeAttribute("autofocus");
66010
+ }
66011
+ if (!autofocusWasSet) {
66012
+ autofocusTarget.setAttribute("autofocus", "");
66013
+ }
66014
+ try {
66015
+ groupEl.showModal();
66016
+ } catch (error) {
66017
+ placeholder.remove();
66018
+ clearPlacement(groupEl);
66019
+ groupEl.removeAttribute("navi-transitions-off");
66020
+ throw error;
66021
+ } finally {
66022
+ if (!autofocusWasSet) {
66023
+ autofocusTarget.removeAttribute("autofocus");
66024
+ }
66025
+ for (const element of autofocusSetAside) {
66026
+ element.setAttribute("autofocus", "");
66027
+ }
66028
+ }
66029
+ // Measured once the group has left the flow, where a parent sized by its
66030
+ // content no longer counts it. Only a box given its width by what holds it
66031
+ // goes on following that width (a rotation while engaged); one sized by its
66032
+ // content, bounded by a min or max width the placeholder does not carry, or
66033
+ // placed by margins read here as px (auto ones, percentages), is held at
66034
+ // the width it has.
66035
+ const placeholderWidth = placeholder.getBoundingClientRect().width;
66036
+ if (parseFloat(marginLeft) !== 0 || parseFloat(marginRight) !== 0 || maxWidth !== "none" || minWidth !== "auto" && parseFloat(minWidth) !== 0 || placeholderWidth < rect.width - 0.5 || placeholderWidth > rect.width + 0.5) {
66037
+ placeholder.style.width = `${rect.width}px`;
66038
+ }
66039
+ // Where the engine focused something else anyway.
66040
+ if (groupEl.scrollTop !== scrollTop) {
66041
+ groupEl.scrollTop = scrollTop;
66042
+ }
66043
+ if (groupEl.scrollLeft !== scrollLeft) {
66044
+ groupEl.scrollLeft = scrollLeft;
66045
+ }
66046
+ if (focusWasInGroup && document.activeElement !== focusedBefore) {
66047
+ moveFocusTo(focusedBefore, {
66048
+ focusVisible: ringBefore
66049
+ });
66050
+ }
66051
+ // What showModal() left focused when the focus came from outside: that is
66052
+ // the browser's pick, not a restore of navi's, at the close (see cleanup).
66053
+ const focusLeftByShowModal = focusWasInGroup ? null : document.activeElement;
66054
+ if (silent) {
66055
+ // The wall's starting style is resolved with no transition to play from
66056
+ // it; giving the transition back afterwards changes no value, so nothing
66057
+ // fades.
66058
+ groupEl.getBoundingClientRect();
66059
+ groupEl.removeAttribute("navi-transitions-off");
66060
+ }
66061
+ // Top-layer order is the order things were shown in: whatever was already
66062
+ // open inside the group (a callout on a peer) is now under it, and goes back
66063
+ // on top.
66064
+ for (const topLayerEl of groupEl.querySelectorAll(":popover-open")) {
66065
+ topLayerEl.hidePopover();
66066
+ topLayerEl.showPopover();
66067
+ }
66068
+ const [cleanups, addCleanup] = createCleanupList();
66069
+ const engagement = {
66070
+ // From a press's pointerdown to its click (or its pointercancel).
66071
+ pressInFlight: false,
66072
+ // A popup closed during the press in flight: its end decides.
66073
+ pressClosed: false,
66074
+ visibleBox,
66075
+ cleanup: ({
66076
+ leavingTree
66077
+ }) => {
66078
+ for (const cleanup of cleanups) {
66079
+ cleanup();
66080
+ }
66081
+ clearPlacement(groupEl);
66082
+ placeholder.remove();
66083
+ if (leavingTree) {
66084
+ // Closed once the commit is over, as a Dialog leaving the tree is (see
66085
+ // its close cleanup): closing now would hand the focus to what held it
66086
+ // at showModal() time, often leaving in this same commit. A park
66087
+ // takes the group out of the top layer and leaves its `open`
66088
+ // attribute, which the reveal would show.
66089
+ queueMicrotask(() => {
66090
+ if (groupEl.open) {
66091
+ groupEl.close();
66092
+ }
66093
+ });
66094
+ return;
66095
+ }
66096
+ // close() hands the focus back to whatever held it at showModal() time
66097
+ // (focusedBefore), undoing where a popup of the group, or the user, put
66098
+ // it since. Unless no popup moved the focus since showModal() did, that
66099
+ // element is made no focus target for the call, so the focus stays where
66100
+ // it is: inert — or, when the focus is inside it (the sheet around the
66101
+ // popover around the group, focused by a press on its blank), without
66102
+ // its tabindex, inert taking the focus with it. A <dialog> stays
66103
+ // focusable without one: around a Dialog sheet, close() focuses it and
66104
+ // the moveFocusTo below brings the focus back.
66105
+ const focusedBeforeClose = document.activeElement;
66106
+ const ringBeforeClose = Boolean(focusedBeforeClose?.matches?.(":focus-visible"));
66107
+ const focusMovedSinceShowModal = focusedBeforeClose && focusedBeforeClose !== document.body && focusedBeforeClose !== groupEl && focusedBeforeClose !== focusLeftByShowModal && focusedBeforeClose.isConnected;
66108
+ const keepsFocusWhereItIs = focusMovedSinceShowModal && focusedBefore && focusedBefore !== focusedBeforeClose && focusedBefore !== document.body && focusedBefore.isConnected;
66109
+ const holdsFocusInside = keepsFocusWhereItIs && focusedBefore.contains(focusedBeforeClose);
66110
+ const madeInert = keepsFocusWhereItIs && !holdsFocusInside && !focusedBefore.inert;
66111
+ const tabindexSetAside = holdsFocusInside && focusedBefore.tagName !== "DIALOG" ? focusedBefore.getAttribute("tabindex") : null;
66112
+ if (madeInert) {
66113
+ focusedBefore.inert = true;
66114
+ }
66115
+ if (tabindexSetAside !== null) {
66116
+ focusedBefore.removeAttribute("tabindex");
66117
+ }
66118
+ try {
66119
+ if (groupEl.open) {
66120
+ groupEl.close();
66121
+ }
66122
+ } finally {
66123
+ if (madeInert) {
66124
+ focusedBefore.inert = false;
66125
+ }
66126
+ if (tabindexSetAside !== null) {
66127
+ focusedBefore.setAttribute("tabindex", tabindexSetAside);
66128
+ }
66129
+ }
66130
+ // Where the engine moved it anyway (an element focusable without a
66131
+ // tabindex).
66132
+ if (focusMovedSinceShowModal && document.activeElement !== focusedBeforeClose) {
66133
+ moveFocusTo(focusedBeforeClose, {
66134
+ focusVisible: ringBeforeClose
66135
+ });
66136
+ }
66137
+ }
66138
+ };
66139
+ let pendingCheck = null;
66140
+ engagement.cancelPendingCheck = () => {
66141
+ if (pendingCheck) {
66142
+ pendingCheck.cancel();
66143
+ pendingCheck = null;
66144
+ }
66145
+ };
66146
+ engagement.checkAfterTask = (delay = 0) => {
66147
+ engagement.cancelPendingCheck();
66148
+ const timeout = setTimeout(() => {
66149
+ pendingCheck = null;
66150
+ group.disengageIfNothingOpen();
66151
+ }, delay);
66152
+ pendingCheck = {
66153
+ cancel: () => {
66154
+ clearTimeout(timeout);
66155
+ }
66156
+ };
66157
+ };
66158
+ engagement.checkAfterMicrotask = () => {
66159
+ engagement.cancelPendingCheck();
66160
+ let cancelled = false;
66161
+ queueMicrotask(() => {
66162
+ if (cancelled) {
66163
+ return;
66164
+ }
66165
+ pendingCheck = null;
66166
+ group.disengageIfNothingOpen();
66167
+ });
66168
+ pendingCheck = {
66169
+ cancel: () => {
66170
+ cancelled = true;
66171
+ }
66172
+ };
66173
+ };
66174
+ addCleanup(engagement.cancelPendingCheck);
66175
+ addCleanup(armPressArbiter(groupEl, group, engagement));
66176
+ addCleanup(followPlaceholder(groupEl, placeholder, engagement));
66177
+ // A wheel or a finger on the wall would scroll the page behind, and with it
66178
+ // the place the group is pinned to. On the wall means outside what is shown
66179
+ // of the group, which a clip can make less than its box. Let go for a
66180
+ // callout only where the group's place scrolls with the viewport: in a
66181
+ // sheet, a list scrolling on its own, a fixed bar, the gesture would scroll
66182
+ // the document behind and never the box the group stands in.
66183
+ let releaseScrollLock = null;
66184
+ const letGoOfScroll = () => {
66185
+ if (releaseScrollLock) {
66186
+ releaseScrollLock();
66187
+ releaseScrollLock = null;
66188
+ }
66189
+ };
66190
+ engagement.followScrollLock = openMemberHoldsPage => {
66191
+ if (!openMemberHoldsPage && placeScrollsWithViewport) {
66192
+ letGoOfScroll();
66193
+ return;
66194
+ }
66195
+ if (!releaseScrollLock) {
66196
+ releaseScrollLock = trapScrollInside(groupEl, {
66197
+ backdrop: true,
66198
+ getElementBox: () => engagement.visibleBox,
66199
+ // The group is no scroll container: shown fixed in the top layer, a
66200
+ // gesture over what it holds, or a scroll key pressed in it, would go
66201
+ // straight on to the page.
66202
+ containInside: true
66203
+ });
66204
+ }
66205
+ };
66206
+ addCleanup(letGoOfScroll);
66207
+ if (themeColor !== "none") {
66208
+ addCleanup(pushPopupThemeColor({
66209
+ themeColor,
66210
+ backdropColor: readBackdropColor(groupEl)
66211
+ }));
66212
+ }
66213
+
66214
+ // Escape and the back gesture reach the topmost modal, which is the group:
66215
+ // they are the open popup's to answer, as they would be with focus in it.
66216
+ // Escape is answered at its keydown, before it becomes the browser's close
66217
+ // request: a cancel can be prevented only while the page has a user
66218
+ // activation to spend, and Escape gives none — at the second one the
66219
+ // browser closes the group itself, whatever the open popup said. A keydown
66220
+ // prevented is no close request at all. Left to what the key was pressed in
66221
+ // when that uses it (a search field clearing itself), as the open popup's
66222
+ // own shortcut leaves it.
66223
+ const onKeyDown = keydownEvent => {
66224
+ if (keydownEvent.key !== "Escape" || keydownEvent.defaultPrevented) {
66225
+ // Answered already: by the open popup's own shortcut, when the focus is
66226
+ // in it.
66227
+ return;
66228
+ }
66229
+ if (keydownEvent.target !== groupEl) {
66230
+ const defaultAction = getKeyboardEventDefaultAction(keydownEvent);
66231
+ if (defaultAction && defaultAction !== "dismiss") {
66232
+ return;
66233
+ }
66234
+ }
66235
+ const openMember = group.getOpenMember();
66236
+ if (!openMember) {
66237
+ return;
66238
+ }
66239
+ // requestClose prevents the keydown, whether the popup closes or refuses.
66240
+ openMember.openController.requestClose(keydownEvent, {
66241
+ isCancel: true
66242
+ });
66243
+ };
66244
+ groupEl.addEventListener("keydown", onKeyDown);
66245
+ addCleanup(() => {
66246
+ groupEl.removeEventListener("keydown", onKeyDown);
66247
+ });
66248
+ // What still reaches the group as a close request: the back gesture, an
66249
+ // Escape with nothing of the group open.
66250
+ const onCancel = cancelEvent => {
66251
+ if (cancelEvent.target !== cancelEvent.currentTarget) {
66252
+ // A file input inside the group cancelling its own picker.
66253
+ return;
66254
+ }
66255
+ cancelEvent.preventDefault();
66256
+ const openMember = group.getOpenMember();
66257
+ if (openMember) {
66258
+ openMember.openController.requestClose(cancelEvent, {
66259
+ isCancel: true
66260
+ });
66261
+ return;
66262
+ }
66263
+ group.disengageIfNothingOpen();
66264
+ };
66265
+ groupEl.addEventListener("cancel", onCancel);
66266
+ addCleanup(() => {
66267
+ groupEl.removeEventListener("cancel", onCancel);
66268
+ });
66269
+ // The browser may close the group itself: a close request with no user
66270
+ // activation left to spend (a second back gesture) ignores preventDefault.
66271
+ const onNativeClose = closeEvent => {
66272
+ if (groupEl.open) {
66273
+ // Queued by an earlier engagement's close(): the group has been shown
66274
+ // modal again since, and nothing closed it.
66275
+ return;
66276
+ }
66277
+ group.closeEverything(closeEvent);
66278
+ };
66279
+ groupEl.addEventListener("close", onNativeClose);
66280
+ addCleanup(() => {
66281
+ groupEl.removeEventListener("close", onNativeClose);
66282
+ });
66283
+ // What holds the group closing (the sheet it is in) does not close the
66284
+ // group: a modal left open in a closed dialog keeps the whole document inert
66285
+ // with nothing on screen to dismiss. A navi popup closes the groups inside it
66286
+ // itself, before it gives the focus back (closePopupGroupsInside); this is
66287
+ // for any other.
66288
+ const openableAncestor = closestOpenableAncestor(groupEl);
66289
+ if (openableAncestor) {
66290
+ addCleanup(observeAncestorOpenState(openableAncestor, ({
66291
+ isOpen
66292
+ }) => {
66293
+ if (isOpen) {
66294
+ return;
66295
+ }
66296
+ group.closeEverything(new CustomEvent("popup_group_ancestor_close", {
66297
+ detail: {}
66298
+ }));
66299
+ }));
66300
+ }
66301
+ return engagement;
66302
+ };
66303
+
66304
+ /**
66305
+ * Hears every press while the group is engaged, for the popup of the group
66306
+ * that is open. A press outside that popup closes it, per its pressOutside,
66307
+ * and goes on to what it landed on: another popup's trigger opens it, a button
66308
+ * does its thing. A press on the wall — the ::backdrop reports the group as its
66309
+ * target, outside its box — closes and reaches nothing else, everything behind
66310
+ * being inert.
66311
+ *
66312
+ * Decided on the press's mousedown, the event the page acts on: on a touch
66313
+ * screen it is synthesized at touchend and hit-tested again, with the group
66314
+ * still up, so a finger that scrolled the group (pointercancel, no mousedown)
66315
+ * closes nothing. A pointerdown cancelled downstream (a drag source) brings no
66316
+ * mousedown: its click decides instead. A popup that captures the outside is
66317
+ * answered at the pointerdown itself: nothing of that press may reach a peer.
66318
+ */
66319
+ const armPressArbiter = (groupEl, group, engagement) => {
66320
+ let press = null;
66321
+ const isOnWall = event => {
66322
+ if (event.target !== groupEl) {
66323
+ return false;
66324
+ }
66325
+ const {
66326
+ left,
66327
+ top,
66328
+ right,
66329
+ bottom
66330
+ } = engagement.visibleBox;
66331
+ return event.clientX < left || event.clientX > right || event.clientY < top || event.clientY > bottom;
66332
+ };
66333
+ const spend = event => {
66334
+ event.preventDefault();
66335
+ event.stopImmediatePropagation();
66336
+ };
66337
+ // The open popup this press is outside of, or null.
66338
+ const findMemberPressedOutside = event => {
66339
+ const openMember = group.getOpenMember();
66340
+ if (!openMember) {
66341
+ return null;
66342
+ }
66343
+ if (openedDuringThisPress(openMember.openController)) {
66344
+ // The release of the press that opened it.
66345
+ return null;
66346
+ }
66347
+ const {
66348
+ target
66349
+ } = event;
66350
+ const {
66351
+ popupElement
66352
+ } = openMember;
66353
+ if (popupElement.contains(target)) {
66354
+ return null;
66355
+ }
66356
+ if (isDeclaredInsidePopup(target, openMember.popupId || popupElement.id)) {
66357
+ return null;
66358
+ }
66359
+ // In front of the open popup: something open inside the group that is
66360
+ // neither that popup nor around it (a callout on a peer).
66361
+ const layerUnderPointer = target.closest?.(':popover-open, dialog[open]:not([navi-popup-group]), [navi-control="popover"][aria-expanded="true"]');
66362
+ if (layerUnderPointer && !layerUnderPointer.contains(popupElement) && !popupElement.contains(layerUnderPointer)) {
66363
+ return null;
66364
+ }
66365
+ return openMember;
66366
+ };
66367
+ const readPressOutside = openMember => readPressOutsideInPopupGroup(openMember.pressOutside);
66368
+ const arbitrate = event => {
66369
+ const openMember = findMemberPressedOutside(event);
66370
+ if (!openMember) {
66371
+ return;
66372
+ }
66373
+ const pressOutside = readPressOutside(openMember);
66374
+ if (pressOutside === "capture") {
66375
+ spend(event);
66376
+ press.spendClick = true;
66377
+ return;
66378
+ }
66379
+ // A press on a peer goes on to it, so the close is told the pointerdown:
66380
+ // told the mousedown, the focus restore cancels that mousedown, and the
66381
+ // peer's own reaction to it with it (a picker opening at mousedown).
66382
+ let closeEvent;
66383
+ if (isOnWall(event)) {
66384
+ closeEvent = event;
66385
+ } else if (event.type === "click" && press.pointerdown) {
66386
+ // Decided at the click (the pointerdown was cancelled): the close is
66387
+ // told both, so what holds a reopen back for this press knows the press
66388
+ // ends with this very click.
66389
+ closeEvent = new CustomEvent("popup_group_press_outside", {
66390
+ detail: {
66391
+ event,
66392
+ eventChain: [press.pointerdown, event]
66393
+ }
66394
+ });
66395
+ } else {
66396
+ closeEvent = press.pointerdown || event;
66397
+ }
66398
+ const closed = openMember.openController.requestClose(closeEvent, {
66399
+ isCancel: pressOutside === "cancel"
66400
+ });
66401
+ if (!closed) {
66402
+ // The popup refuses to be left (a value it cannot note): the press must
66403
+ // not go on to open a peer next to it.
66404
+ spend(event);
66405
+ press.spendClick = true;
66406
+ }
66407
+ };
66408
+ const onPointerDown = pointerDownEvent => {
66409
+ if (engagement.pressClosed) {
66410
+ // The press that closed the last popup ended without a click (a gesture
66411
+ // swallowed it): this one is a new press.
66412
+ engagement.pressClosed = false;
66413
+ group.disengageIfNothingOpen();
66414
+ if (!groupEl.open) {
66415
+ return;
66416
+ }
66417
+ }
66418
+ press = {
66419
+ pointerdown: pointerDownEvent,
66420
+ mousedown: null,
66421
+ spendClick: false
66422
+ };
66423
+ if (pointerDownEvent.button !== 0) {
66424
+ return;
66425
+ }
66426
+ engagement.pressInFlight = true;
66427
+ const openMember = findMemberPressedOutside(pointerDownEvent);
66428
+ if (openMember && readPressOutside(openMember) === "capture") {
66429
+ // Cancelling the pointerdown also cancels the mousedown it would bring.
66430
+ spend(pointerDownEvent);
66431
+ press.spendClick = true;
66432
+ }
66433
+ };
66434
+ const onMouseDown = mouseDownEvent => {
66435
+ if (mouseDownEvent.button !== 0) {
66436
+ return;
66437
+ }
66438
+ if (!press) {
66439
+ press = {
66440
+ pointerdown: null,
66441
+ mousedown: null,
66442
+ spendClick: false
66443
+ };
66444
+ }
66445
+ engagement.pressInFlight = true;
66446
+ press.mousedown = mouseDownEvent;
66447
+ if (press.spendClick) {
66448
+ spend(mouseDownEvent);
66449
+ return;
66450
+ }
66451
+ arbitrate(mouseDownEvent);
66452
+ };
66453
+ const onClick = clickEvent => {
66454
+ if (!isPressDrivenClick(clickEvent)) {
66455
+ // A keyboard activation or a click() from code: not the click of the
66456
+ // press in flight, which the next pointerdown replaces anyway.
66457
+ return;
66458
+ }
66459
+ if (press?.spendClick) {
66460
+ spend(clickEvent);
66461
+ } else if (press && !press.mousedown) {
66462
+ arbitrate(clickEvent);
66463
+ }
66464
+ press = null;
66465
+ engagement.pressInFlight = false;
66466
+ if (engagement.pressClosed) {
66467
+ engagement.pressClosed = false;
66468
+ // After the click's dispatch: a peer it opens counts.
66469
+ engagement.checkAfterTask();
66470
+ }
66471
+ };
66472
+ const onPointerUp = () => {
66473
+ if (engagement.pressClosed) {
66474
+ engagement.checkAfterTask(END_OF_PRESS_SAFETY_MS);
66475
+ }
66476
+ };
66477
+ const onPointerCancel = () => {
66478
+ press = null;
66479
+ engagement.pressInFlight = false;
66480
+ if (engagement.pressClosed) {
66481
+ engagement.pressClosed = false;
66482
+ engagement.checkAfterTask();
66483
+ }
66484
+ };
66485
+ const listeners = [["pointerdown", onPointerDown], ["mousedown", onMouseDown], ["click", onClick], ["pointerup", onPointerUp], ["pointercancel", onPointerCancel]];
66486
+ for (const [type, listener] of listeners) {
66487
+ document.addEventListener(type, listener, {
66488
+ capture: true
66489
+ });
66490
+ }
66491
+ return () => {
66492
+ for (const [type, listener] of listeners) {
66493
+ document.removeEventListener(type, listener, {
66494
+ capture: true
66495
+ });
66496
+ }
66497
+ };
66498
+ };
66499
+
66500
+ // The group follows the place it holds in the page: a scroll of what holds it,
66501
+ // the viewport resizing (a keyboard, a rotation), the page reflowing around
66502
+ // it, a popup it is in being placed again. Its own scroll moves nothing.
66503
+ const followPlaceholder = (groupEl, placeholder, engagement) => {
66504
+ const [cleanups, addCleanup] = createCleanupList();
66505
+ // An ancestor's movement may end after this engagement did, and the group a
66506
+ // later one engaged is the same element.
66507
+ let disposed = false;
66508
+ addCleanup(() => {
66509
+ disposed = true;
66510
+ });
66511
+ const update = () => {
66512
+ if (disposed) {
66513
+ return;
66514
+ }
66515
+ const rect = placeholder.getBoundingClientRect();
66516
+ const ancestorClip = readAncestorClip(placeholder, getComputedStyle(placeholder));
66517
+ engagement.visibleBox = writePlacement(groupEl, rect, ancestorClip);
66518
+ };
66519
+ const onScroll = scrollEvent => {
66520
+ if (scrollEvent.target === groupEl || groupEl.contains(scrollEvent.target)) {
66521
+ return;
66522
+ }
66523
+ update();
66524
+ };
66525
+ document.addEventListener("scroll", onScroll, {
66526
+ capture: true,
66527
+ passive: true
66528
+ });
66529
+ window.addEventListener("resize", update);
66530
+ const {
66531
+ visualViewport
66532
+ } = window;
66533
+ visualViewport?.addEventListener("resize", update);
66534
+ addCleanup(() => {
66535
+ document.removeEventListener("scroll", onScroll, {
66536
+ capture: true
66537
+ });
66538
+ window.removeEventListener("resize", update);
66539
+ visualViewport?.removeEventListener("resize", update);
66540
+ });
66541
+ const resizeObserver = new ResizeObserver(update);
66542
+ if (placeholder.parentElement) {
66543
+ resizeObserver.observe(placeholder.parentElement);
66544
+ }
66545
+ addCleanup(() => {
66546
+ resizeObserver.disconnect();
66547
+ });
66548
+ // A popup the group is in moves by a translate, animated, and says so only
66549
+ // to itself (applyNewPosition): followed frame by frame while it moves, as
66550
+ // visibleRectEffect follows it for what is anchored inside it.
66551
+ let ancestor = closestOpenableAncestor(groupEl);
66552
+ while (ancestor) {
66553
+ const openableAncestor = ancestor;
66554
+ let positionTransitionFrame = null;
66555
+ const onPositionChange = () => {
66556
+ update();
66557
+ };
66558
+ const onPositionTransition = positionTransitionEvent => {
66559
+ cancelAnimationFrame(positionTransitionFrame);
66560
+ const loop = () => {
66561
+ update();
66562
+ positionTransitionFrame = requestAnimationFrame(loop);
66563
+ };
66564
+ loop();
66565
+ positionTransitionEvent.detail.onEnd(() => {
66566
+ cancelAnimationFrame(positionTransitionFrame);
66567
+ positionTransitionFrame = null;
66568
+ update();
66569
+ });
66570
+ };
66571
+ openableAncestor.addEventListener("navi_position_change", onPositionChange);
66572
+ openableAncestor.addEventListener("navi_position_transition", onPositionTransition);
66573
+ addCleanup(() => {
66574
+ cancelAnimationFrame(positionTransitionFrame);
66575
+ openableAncestor.removeEventListener("navi_position_change", onPositionChange);
66576
+ openableAncestor.removeEventListener("navi_position_transition", onPositionTransition);
66577
+ });
66578
+ ancestor = closestOpenableAncestor(openableAncestor);
66579
+ }
66580
+ return () => {
66581
+ for (const cleanup of cleanups) {
66582
+ cleanup();
66583
+ }
66584
+ };
66585
+ };
66586
+
66587
+ // What stands in the page for the group while it is in the top layer: the same
66588
+ // outer box at the height it had, invisible, counted by nothing that skips
66589
+ // what navi puts in a tree (css_architecture.md, "Counting children").
66590
+ const createPlaceholder = (groupStyle, rect, baselineDescent) => {
66591
+ const placeholder = document.createElement("div");
66592
+ placeholder.setAttribute("navi-popup-group-placeholder", "");
66593
+ placeholder.setAttribute("navi-out-of-flow", "");
66594
+ placeholder.setAttribute("aria-hidden", "true");
66595
+ const {
66596
+ style
66597
+ } = placeholder;
66598
+ if (groupStyle.display.startsWith("inline")) {
66599
+ style.display = "inline-block";
66600
+ } else if (establishesFormattingContext(groupStyle)) {
66601
+ // Such a box keeps clear of a float beside it, where a plain block slides
66602
+ // under it.
66603
+ style.display = "flow-root";
66604
+ } else {
66605
+ style.display = "block";
66606
+ }
66607
+ style.visibility = "hidden";
66608
+ style.pointerEvents = "none";
66609
+ style.boxSizing = "border-box";
66610
+ style.height = `${rect.height}px`;
66611
+ style.flex = "none";
66612
+ for (const property of PLACEHOLDER_COPIED_PROPERTIES) {
66613
+ style[property] = groupStyle[property];
66614
+ }
66615
+ if (baselineDescent !== null) {
66616
+ // An empty inline box sits on its bottom edge; the group sat on its own
66617
+ // baseline, this far above its bottom.
66618
+ style.verticalAlign = `${-baselineDescent}px`;
66619
+ }
66620
+ return placeholder;
66621
+ };
66622
+ const establishesFormattingContext = style => {
66623
+ const {
66624
+ display,
66625
+ overflowX,
66626
+ overflowY,
66627
+ contain,
66628
+ containerType,
66629
+ contentVisibility,
66630
+ columnCount,
66631
+ columnWidth
66632
+ } = style;
66633
+ if (display === "flow-root" || display === "flex" || display === "grid" || display === "table" || display.includes("flow-root")) {
66634
+ return true;
66635
+ }
66636
+ if (overflowX !== "visible" && overflowX !== "clip" || overflowY !== "visible" && overflowY !== "clip") {
66637
+ return true;
66638
+ }
66639
+ // Containment the computed `contain` does not show: a size container, a box
66640
+ // whose content-visibility skips its content.
66641
+ if (/\b(?:size|inline-size)\b/.test(containerType) || contentVisibility === "auto" || contentVisibility === "hidden") {
66642
+ return true;
66643
+ }
66644
+ // A multi-column box.
66645
+ if (columnCount !== "auto" || columnWidth !== "auto") {
66646
+ return true;
66647
+ }
66648
+ return /\b(?:layout|paint|strict|content)\b/.test(contain);
66649
+ };
66650
+ const PLACEHOLDER_COPIED_PROPERTIES = ["position", "top", "right", "bottom", "left", "marginTop", "marginRight", "marginBottom", "marginLeft", "alignSelf", "justifySelf", "order", "gridRowStart", "gridRowEnd", "gridColumnStart", "gridColumnEnd", "float", "verticalAlign"];
66651
+
66652
+ // How far below the line's baseline an inline-level group on that baseline
66653
+ // reaches: what the placeholder, which has no line of its own, has to sink by.
66654
+ const readBaselineDescent = (groupEl, groupStyle, rect) => {
66655
+ if (!groupStyle.display.startsWith("inline") || groupStyle.verticalAlign !== "baseline") {
66656
+ return null;
66657
+ }
66658
+ const marginTop = parseFloat(groupStyle.marginTop) || 0;
66659
+ const marginBottom = parseFloat(groupStyle.marginBottom) || 0;
66660
+ const readBaselineY = insert => {
66661
+ const probe = document.createElement("span");
66662
+ probe.style.cssText = "display:inline-block;width:0;height:0;vertical-align:baseline";
66663
+ insert(probe);
66664
+ const baselineY = probe.getBoundingClientRect().bottom;
66665
+ probe.remove();
66666
+ return baselineY;
66667
+ };
66668
+ // On the group's own line, between its top and its bottom margin edge: a
66669
+ // probe that takes no room can stay at the end of the line before it.
66670
+ const isOnGroupLine = y => y >= rect.top - marginTop - 0.5 && y <= rect.bottom + marginBottom + 0.5;
66671
+ let baselineY = readBaselineY(probe => groupEl.before(probe));
66672
+ if (!isOnGroupLine(baselineY)) {
66673
+ baselineY = readBaselineY(probe => groupEl.after(probe));
66674
+ if (!isOnGroupLine(baselineY)) {
66675
+ return null;
66676
+ }
66677
+ }
66678
+ // Down to the margin edge: that is what an empty inline box sits on.
66679
+ return rect.bottom + marginBottom - baselineY;
66680
+ };
66681
+
66682
+ // Writes where the group stands and how its ancestors clip it; returns what of
66683
+ // it is on screen.
66684
+ const writePlacement = (groupEl, rect, ancestorClip) => {
66685
+ const {
66686
+ style
66687
+ } = groupEl;
66688
+ style.setProperty("--x-popup-group-top", `${rect.top}px`);
66689
+ style.setProperty("--x-popup-group-left", `${rect.left}px`);
66690
+ style.setProperty("--x-popup-group-width", `${rect.width}px`);
66691
+ style.setProperty("--x-popup-group-height", `${rect.height}px`);
66692
+ if (ancestorClip) {
66693
+ // From the group's border box, reaching past it on a side nothing clips:
66694
+ // what overflows the group was painted up to its ancestors' edge.
66695
+ const toInset = distance => distance === -Infinity ? "-100vmax" : `${distance}px`;
66696
+ style.setProperty("--x-popup-group-clip", `inset(${toInset(ancestorClip.top - rect.top)} ${toInset(rect.right - ancestorClip.right)} ${toInset(rect.bottom - ancestorClip.bottom)} ${toInset(ancestorClip.left - rect.left)})`);
66697
+ } else {
66698
+ style.removeProperty("--x-popup-group-clip");
66699
+ }
66700
+ return intersectBox(rect, ancestorClip);
66701
+ };
66702
+ const clearPlacement = groupEl => {
66703
+ const {
66704
+ style
66705
+ } = groupEl;
66706
+ style.removeProperty("--x-popup-group-top");
66707
+ style.removeProperty("--x-popup-group-left");
66708
+ style.removeProperty("--x-popup-group-width");
66709
+ style.removeProperty("--x-popup-group-height");
66710
+ style.removeProperty("--x-popup-group-padding");
66711
+ style.removeProperty("--x-popup-group-clip");
66712
+ };
66713
+
66714
+ // The clip the ancestors of `element` put on it, up to the first box nothing
66715
+ // around clips (a top-layer or fixed one), -Infinity/Infinity on a side
66716
+ // nothing clips — or null when nothing clips it at all.
66717
+ const readAncestorClip = (element, elementStyle) => {
66718
+ if (elementStyle.position === "fixed") {
66719
+ return null;
66720
+ }
66721
+ // Read on body, an overflow that is the viewport's clips nothing.
66722
+ const bodyClipsNothing = bodyOverflowIsTheViewports();
66723
+ let left = -Infinity;
66724
+ let top = -Infinity;
66725
+ let right = Infinity;
66726
+ let bottom = Infinity;
66727
+ let clipped = false;
66728
+ // An absolutely placed box escapes the clip of every ancestor between it
66729
+ // and its containing block: those are skipped.
66730
+ let skipUntilContainingBlock = elementStyle.position === "absolute";
66731
+ let ancestor = element.parentElement;
66732
+ while (ancestor && ancestor !== document.documentElement) {
66733
+ const ancestorStyle = getComputedStyle(ancestor);
66734
+ if (skipUntilContainingBlock && !isContainingBlockForAbsolute(ancestorStyle)) {
66735
+ ancestor = ancestor.parentElement;
66736
+ continue;
66737
+ }
66738
+ skipUntilContainingBlock = false;
66739
+ const {
66740
+ overflowX,
66741
+ overflowY,
66742
+ position
66743
+ } = ancestorStyle;
66744
+ const overflowGoesToViewport = ancestor === document.body && bodyClipsNothing;
66745
+ const clipsX = !overflowGoesToViewport && overflowX !== "visible";
66746
+ const clipsY = !overflowGoesToViewport && overflowY !== "visible";
66747
+ if (clipsX || clipsY) {
66748
+ clipped = true;
66749
+ const ancestorRect = ancestor.getBoundingClientRect();
66750
+ const innerLeft = ancestorRect.left + ancestor.clientLeft;
66751
+ const innerTop = ancestorRect.top + ancestor.clientTop;
66752
+ if (clipsX) {
66753
+ const innerRight = innerLeft + ancestor.clientWidth;
66754
+ if (innerLeft > left) {
66755
+ left = innerLeft;
66756
+ }
66757
+ if (innerRight < right) {
66758
+ right = innerRight;
66759
+ }
66760
+ }
66761
+ if (clipsY) {
66762
+ const innerBottom = innerTop + ancestor.clientHeight;
66763
+ if (innerTop > top) {
66764
+ top = innerTop;
66765
+ }
66766
+ if (innerBottom < bottom) {
66767
+ bottom = innerBottom;
66768
+ }
66769
+ }
66770
+ }
66771
+ if (position === "fixed" || ancestor.matches(":popover-open, dialog:modal")) {
66772
+ break;
66773
+ }
66774
+ if (position === "absolute") {
66775
+ skipUntilContainingBlock = true;
66776
+ }
66777
+ ancestor = ancestor.parentElement;
66778
+ }
66779
+ return clipped ? {
66780
+ left,
66781
+ top,
66782
+ right,
66783
+ bottom
66784
+ } : null;
66785
+ };
66786
+ // The overflow of <body> is the viewport's when the root leaves its own
66787
+ // visible: body itself then neither scrolls nor clips.
66788
+ const bodyOverflowIsTheViewports = () => {
66789
+ const rootStyle = getComputedStyle(document.documentElement);
66790
+ return rootStyle.overflowX === "visible" && rootStyle.overflowY === "visible";
66791
+ };
66792
+ const isContainingBlockForAbsolute = style => style.position !== "static" || style.transform !== "none" || style.translate !== "none" || style.rotate !== "none" || style.scale !== "none" || style.perspective !== "none" || style.filter !== "none" ||
66793
+ // Prefixed alone before Safari 18.
66794
+ (style.backdropFilter ?? style.webkitBackdropFilter ?? "none") !== "none" || /\b(?:layout|paint|strict|content)\b/.test(style.contain) ||
66795
+ // Layout containment the computed `contain` does not show.
66796
+ style.contentVisibility === "auto" || /\b(?:size|inline-size)\b/.test(style.containerType) || /\b(?:transform|translate|rotate|scale|perspective|filter|backdrop-filter|contain)\b/.test(style.willChange);
66797
+ const intersectBox = (rect, clip) => {
66798
+ let {
66799
+ left,
66800
+ top,
66801
+ right,
66802
+ bottom
66803
+ } = rect;
66804
+ if (clip) {
66805
+ if (clip.left > left) {
66806
+ left = clip.left;
66807
+ }
66808
+ if (clip.top > top) {
66809
+ top = clip.top;
66810
+ }
66811
+ if (clip.right < right) {
66812
+ right = clip.right;
66813
+ }
66814
+ if (clip.bottom < bottom) {
66815
+ bottom = clip.bottom;
66816
+ }
66817
+ }
66818
+ if (right < left) {
66819
+ right = left;
66820
+ }
66821
+ if (bottom < top) {
66822
+ bottom = top;
66823
+ }
66824
+ return {
66825
+ left,
66826
+ top,
66827
+ right,
66828
+ bottom
66829
+ };
66830
+ };
66831
+ const createCleanupList = () => {
66832
+ const cleanups = [];
66833
+ return [cleanups, cleanup => {
66834
+ cleanups.push(cleanup);
66835
+ }];
66836
+ };
66837
+
65547
66838
  /**
65548
66839
  * Where the "popover or dialog?" answer lives, for both the components that
65549
66840
  * decide it and the content that renders inside one.
@@ -67294,7 +68585,8 @@ const css$E = /* css */`
67294
68585
  * wall is shown through the Popover API rather than `showModal()`, which is
67295
68586
  * what a sheet docked to the screen's bottom edge over a still-readable map
67296
68587
  * needs. What it gives up is what only a modal gets natively: the hardware
67297
- * back button no longer dismisses it. See docs/popup_backdrop.md.
68588
+ * back button no longer dismisses it. Not read inside a `<PopupGroup>`,
68589
+ * whose wall it is. See docs/popup_backdrop.md.
67298
68590
  * @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="close"]
67299
68591
  * - `"close"` closes the dialog on an outside press, `"cancel"` closes it
67300
68592
  * as a cancel (what it holds reverts, as on Escape). `"capture"`/`"ignore"`
@@ -67851,7 +69143,7 @@ const useDialogProps = props => {
67851
69143
  // before how it is painted (backdropVariant below). It also picks the show
67852
69144
  // call for layer="top": a wall is what showModal() is for, and without one
67853
69145
  // the dialog goes to the same top layer through the Popover API instead.
67854
- backdrop = true,
69146
+ backdrop: backdropProp = true,
67855
69147
  // How loudly the backdrop says it is there — independent of what it
67856
69148
  // *does* (that's pressOutside above). "lift" is the
67857
69149
  // opaque wall at one end, "invisible" a wall that is not seen at the
@@ -67919,9 +69211,20 @@ const useDialogProps = props => {
67919
69211
  // Where it is placed, and whether there is a wall — the two questions that
67920
69212
  // pick the show call (see this file's top comment).
67921
69213
  const isTopLayer = layer === "top";
69214
+ // Inside a <PopupGroup> the wall is the group's (see popup_group.jsx): the
69215
+ // dialog takes the wall-less path, never showModal() — a modal dialog makes
69216
+ // everything outside it inert, the group's other popups included.
69217
+ const popupGroup = useContext(PopupGroupContext);
69218
+ useLayoutEffect(() => {
69219
+ if (!popupGroup) {
69220
+ return undefined;
69221
+ }
69222
+ return popupGroup.register(openController);
69223
+ }, [popupGroup, openController]);
69224
+ const backdrop = backdropProp && !popupGroup;
67922
69225
  const isModal = isTopLayer && backdrop;
67923
69226
  const isTopLayerPopover = isTopLayer && !backdrop;
67924
- if (!backdrop && pressOutside === "capture") {
69227
+ if (!popupGroup && !backdrop && pressOutside === "capture") {
67925
69228
  console.warn(`Dialog: pressOutside="capture" needs a backdrop. Absorbing a press is what a wall does, and backdrop={false} takes it away.`);
67926
69229
  }
67927
69230
  const ref = props.ref;
@@ -68158,6 +69461,14 @@ const useDialogProps = props => {
68158
69461
  if (!dialogEl) {
68159
69462
  return undefined;
68160
69463
  }
69464
+ // First, so the group already stands where it stands in the top layer
69465
+ // when this dialog shows itself above it.
69466
+ popupGroup?.engage({
69467
+ openController,
69468
+ popupElement: dialogEl,
69469
+ pressOutside,
69470
+ event: e
69471
+ });
68161
69472
 
68162
69473
  // Set by useOpenControllerByProps for the very first open triggered by
68163
69474
  // `open`/`defaultOpen` already being truthy at mount, and for an open a
@@ -68248,7 +69559,8 @@ const useDialogProps = props => {
68248
69559
  // A modal dialog always has its own ::backdrop; a local one has the
68249
69560
  // backdrop element when it renders one.
68250
69561
  addCleanup(trapScrollInside(dialogEl, {
68251
- backdrop: isModal || backdropEl
69562
+ // A group's ::backdrop covers the page while this dialog is open.
69563
+ backdrop: popupGroup ? true : isModal || backdropEl
68252
69564
  }));
68253
69565
  } else if (!isTopLayer) {
68254
69566
  // A local dialog is confined to its positioned ancestor, and so is its
@@ -68479,7 +69791,9 @@ const useDialogProps = props => {
68479
69791
  // file's top comment), and a local one the caller asked to leave the page
68480
69792
  // reachable. Active for the dialog's entire open lifetime, not just
68481
69793
  // mid-transition.
68482
- if ((isModal || !backdrop) && (pressOutside === "close" || pressOutside === "cancel")) {
69794
+ if ((isModal || !backdrop) &&
69795
+ // The group's wall is up, and the group hears the press itself.
69796
+ !popupGroup && (pressOutside === "close" || pressOutside === "cancel")) {
68483
69797
  addCleanup(armOutsidePressClose(dialogEl, {
68484
69798
  openController,
68485
69799
  pressOutside,
@@ -68518,6 +69832,11 @@ const useDialogProps = props => {
68518
69832
  dialogEl.close();
68519
69833
  }
68520
69834
  };
69835
+ if (!findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
69836
+ // Before its own focus restore: a group engaged inside it is the
69837
+ // topmost modal, and keeps everything else inert until it goes.
69838
+ closePopupGroupsInside(dialogEl, closeEvent);
69839
+ }
68521
69840
  if (findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
68522
69841
  // Leaving the tree: hidden natively once the commit is over. Its
68523
69842
  // removal takes it out of the top layer and hands the focus to
@@ -68547,9 +69866,11 @@ const useDialogProps = props => {
68547
69866
  if (backdropEl) {
68548
69867
  backdropEl.setAttribute("aria-expanded", "false");
68549
69868
  disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent, () => {
69869
+ disarmBackdropHideRef.current = null;
68550
69870
  backdropEl.setAttribute("navi-hidden", "");
68551
69871
  });
68552
69872
  }
69873
+ popupGroup?.release(openController, closeEvent);
68553
69874
  restoreFocus(closeEvent);
68554
69875
  cleanup();
68555
69876
  if (silent) {
@@ -68610,8 +69931,9 @@ const useDialogProps = props => {
68610
69931
  // there's no gap for the browser to ever paint this plain-div backdrop
68611
69932
  // visible before anything has actually opened it — see popover.jsx's
68612
69933
  // own identical prop for the full reasoning, and this file's own CSS
68613
- // for the rule it drives.
68614
- "navi-hidden": openController.openedInDom ? undefined : "",
69934
+ // for the rule it drives. Left up while the press that closed the dialog
69935
+ // has its click still to come, as popover.jsx's own wall is.
69936
+ "navi-hidden": openController.openedInDom || disarmBackdropHideRef.current ? undefined : "",
68615
69937
  "styleCSSVars": DIALOG_STYLE_CSS_VARS,
68616
69938
  "animationDuration": rest.animationDuration,
68617
69939
  "data-press-outside": pressOutside,
@@ -68738,7 +70060,8 @@ const useDialogProps = props => {
68738
70060
  handlePressOnOutsideRegion(e, {
68739
70061
  popupEl: ref.current,
68740
70062
  openController,
68741
- pressOutside
70063
+ // In a group, the press the group's wall would get.
70064
+ pressOutside: popupGroup ? readPressOutsideInPopupGroup(pressOutside) : pressOutside
68742
70065
  });
68743
70066
  },
68744
70067
  "onKeyDown": e => {
@@ -68766,7 +70089,12 @@ const useDialogProps = props => {
68766
70089
  // prevented it already (see requestClose).
68767
70090
  e.preventDefault();
68768
70091
  },
68769
- children
70092
+ // A popup opened from inside this one is in front of it, not a peer of
70093
+ // it: the group stops here.
70094
+ "children": popupGroup ? jsx(PopupGroupContext.Provider, {
70095
+ value: null,
70096
+ children: children
70097
+ }) : children
68770
70098
  });
68771
70099
 
68772
70100
  // Outside-click handling for the wall we render ourselves. A modal's is a
@@ -69260,13 +70588,6 @@ const css$D = /* css */`
69260
70588
  * is a `Popover`-level answer only for `"close"`/`"cancel"`; `"capture"` has
69261
70589
  * nothing left to absorb without a wall, and `"ignore"` renders no wall anyway.
69262
70590
  * See docs/popup_backdrop.md.
69263
- * @param {string} [props.group] - The popover's peers: the triggers carrying
69264
- * `data-navi-popup-group` with this name. A press on one, while the popover
69265
- * is open, closes it and opens the peer's popup in that same press — a
69266
- * menu bar, where pressing another menu switches to it. The wall still
69267
- * takes the press (the page behind stays out of reach, hover included) and
69268
- * hands its click to the peer it covered. Without a wall there is nothing
69269
- * to ask: the press reaches the peer on its own.
69270
70591
  * @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="ignore"]
69271
70592
  * - `"ignore"` (default): no backdrop at all, outside presses pass straight
69272
70593
  * through. `"close"` closes the popover on an outside press, `"cancel"`
@@ -69654,7 +70975,6 @@ const usePopoverProps = props => {
69654
70975
  // Read at the opening, with the backdrop it would otherwise be worked out
69655
70976
  // from (see openEffect).
69656
70977
  themeColor,
69657
- group,
69658
70978
  scrollCapture,
69659
70979
  focusCapture,
69660
70980
  // "auto" (default) → the popover follows its content. "frozen" → measured
@@ -69716,8 +71036,18 @@ const usePopoverProps = props => {
69716
71036
  // animation={true} or "auto" always resolves to "sliding" or "scaling"
69717
71037
  // (see resolveAutoAnimationKind).
69718
71038
  const isAutoAnimation = animation === true || animation === "auto";
69719
- const hasBackdrop = pressOutside !== "ignore" && backdrop;
69720
- if (!backdrop && pressOutside === "capture") {
71039
+
71040
+ // Inside a <PopupGroup> the wall is the group's (see popup_group.jsx): this
71041
+ // popover draws none, and the group decides what a press outside does.
71042
+ const popupGroup = useContext(PopupGroupContext);
71043
+ useLayoutEffect(() => {
71044
+ if (!popupGroup) {
71045
+ return undefined;
71046
+ }
71047
+ return popupGroup.register(openController);
71048
+ }, [popupGroup, openController]);
71049
+ const hasBackdrop = pressOutside !== "ignore" && backdrop && !popupGroup;
71050
+ if (!popupGroup && !backdrop && pressOutside === "capture") {
69721
71051
  console.warn(`Popover: pressOutside="capture" needs a backdrop. Absorbing a press is what a wall does, and backdrop={false} takes it away — every press outside reaches the page, exactly as with "ignore".`);
69722
71052
  }
69723
71053
  // positionPopover lives in openEffect's closure — created once, when the
@@ -69781,6 +71111,14 @@ const usePopoverProps = props => {
69781
71111
  if (!popoverEl) {
69782
71112
  return undefined;
69783
71113
  }
71114
+ // First, so the group already stands where it stands in the top layer
71115
+ // when this popover reads its anchor and shows itself above it.
71116
+ popupGroup?.engage({
71117
+ openController,
71118
+ popupElement: popoverEl,
71119
+ pressOutside,
71120
+ event: e
71121
+ });
69784
71122
 
69785
71123
  // Set by useOpenControllerByProps for the very first open triggered by
69786
71124
  // `open`/`defaultOpen` already being truthy at mount — there's nothing
@@ -70068,8 +71406,9 @@ const usePopoverProps = props => {
70068
71406
 
70069
71407
  // With no backdrop of its own to catch a press, the popover hears an
70070
71408
  // outside one from the document and takes nothing from it: it closes, and
70071
- // the press goes on to whatever it landed on (armOutsidePressClose).
70072
- if (!hasBackdrop && (pressOutside === "close" || pressOutside === "cancel")) {
71409
+ // the press goes on to whatever it landed on (armOutsidePressClose). Not
71410
+ // inside a group: the group's wall is up, and the group hears the press.
71411
+ if (!hasBackdrop && !popupGroup && (pressOutside === "close" || pressOutside === "cancel")) {
70073
71412
  addCleanup(armOutsidePressClose(popoverEl, {
70074
71413
  openController,
70075
71414
  pressOutside
@@ -70077,7 +71416,8 @@ const usePopoverProps = props => {
70077
71416
  }
70078
71417
  if (scrollCapture) {
70079
71418
  addCleanup(trapScrollInside(popoverEl, {
70080
- backdrop: backdropEl
71419
+ // A group's ::backdrop covers the page while this popover is open.
71420
+ backdrop: popupGroup ? true : backdropEl
70081
71421
  }));
70082
71422
  }
70083
71423
  if (focusCapture) {
@@ -70297,6 +71637,11 @@ const usePopoverProps = props => {
70297
71637
  // comment (openEffect above) for why hidePopover() alone isn't
70298
71638
  // reliably sufficient once a consumer's own CSS also sets display.
70299
71639
  popoverEl.setAttribute("navi-hidden", "");
71640
+ if (!findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
71641
+ // Before its own focus restore: a group engaged inside it is the
71642
+ // topmost modal, and keeps everything else inert until it goes.
71643
+ closePopupGroupsInside(popoverEl, closeEvent);
71644
+ }
70300
71645
  if (isTopLayer) {
70301
71646
  // Left to its removal when the popover leaves the tree: that hides it
70302
71647
  // and hands the focus to nobody, where hidePopover() hands the focus
@@ -70323,23 +71668,17 @@ const usePopoverProps = props => {
70323
71668
  }
70324
71669
  if (backdropEl) {
70325
71670
  backdropEl.setAttribute("aria-expanded", "false");
70326
- disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent, clickEvent => {
71671
+ disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent, () => {
71672
+ disarmBackdropHideRef.current = null;
70327
71673
  // Set regardless of isTopLayer — see openEffect's own identical
70328
71674
  // comment for why hidePopover() alone isn't reliably sufficient.
70329
71675
  backdropEl.setAttribute("navi-hidden", "");
70330
71676
  if (isTopLayer) {
70331
71677
  backdropEl.hidePopover();
70332
71678
  }
70333
- if (group && clickEvent) {
70334
- handWallClickToPeer(clickEvent, {
70335
- wallEl: backdropEl,
70336
- popupEl: popoverEl,
70337
- anchorEl: anchorElement,
70338
- group
70339
- });
70340
- }
70341
71679
  });
70342
71680
  }
71681
+ popupGroup?.release(openController, closeEvent);
70343
71682
  restoreFocus?.(closeEvent);
70344
71683
 
70345
71684
  // pickPositionRelativeTo (visible_rect.js) writes data-position-y/x-
@@ -70418,8 +71757,11 @@ const usePopoverProps = props => {
70418
71757
  // Read fresh on every render (not frozen at mount), so it stays
70419
71758
  // correct even across a re-render that happens to occur while open —
70420
71759
  // see contentProps' own identical prop just below for the full
70421
- // reasoning (kept once, not repeated here).
70422
- "navi-hidden": openController.openedInDom ? undefined : "",
71760
+ // reasoning (kept once, not repeated here). The wall outlives its popup
71761
+ // by the press that closed it (armPointerDownOutsideClose): a render
71762
+ // before that press's click must leave it up — a popup's owner commonly
71763
+ // renders on its close, in a microtask the release comes after.
71764
+ "navi-hidden": openController.openedInDom || disarmBackdropHideRef.current ? undefined : "",
70423
71765
  "styleCSSVars": POPUP_STYLE_CSS_VARS,
70424
71766
  "animationDuration": rest.animationDuration,
70425
71767
  "data-press-outside": pressOutside,
@@ -70517,14 +71859,20 @@ const usePopoverProps = props => {
70517
71859
  handlePressOnOutsideRegion(e, {
70518
71860
  popupEl: ref.current,
70519
71861
  openController,
70520
- pressOutside
71862
+ // In a group, the press the group's wall would get.
71863
+ pressOutside: popupGroup ? readPressOutsideInPopupGroup(pressOutside) : pressOutside
70521
71864
  });
70522
71865
  },
70523
71866
  "onKeyDown": e => {
70524
71867
  onKeyDown?.(e);
70525
71868
  onKeyDownShortcuts(e);
70526
71869
  },
70527
- children
71870
+ // A popup opened from inside this one is in front of it, not a peer of
71871
+ // it: the group stops here.
71872
+ "children": popupGroup ? jsx(PopupGroupContext.Provider, {
71873
+ value: null,
71874
+ children: children
71875
+ }) : children
70528
71876
  // onnavi_request_open/onnavi_request_close/onnavi_open_press: for the
70529
71877
  // uncontrolled case,
70530
71878
  // already arrive here as plain props via ...rest (wired by
@@ -70774,10 +72122,6 @@ const Popup = props => {
70774
72122
  scrollCapture,
70775
72123
  positionAreaFixed,
70776
72124
  positionAreaWhenAnchorIsInvalid,
70777
- // A top-layer dialog's wall is its native ::backdrop, which goes with the
70778
- // dialog on the press that closes it: no click is left on it to hand to a
70779
- // peer (see handWallClickToPeer).
70780
- group,
70781
72125
  // Dialog-only, destructured out for the same reason: forwarded below only
70782
72126
  // in the dialog branch, so they never reach the popover element.
70783
72127
  dockedOnSmallTouchScreen,
@@ -70827,7 +72171,6 @@ const Popup = props => {
70827
72171
  positionAreaWhenAnchorIsInvalid: positionAreaWhenAnchorIsInvalid,
70828
72172
  scrollCapture: scrollCapture === "popover" || scrollCapture,
70829
72173
  positionAreaFixed: positionAreaFixed,
70830
- group: group,
70831
72174
  className: withPropsClassName("navi_popup", className),
70832
72175
  expand: expand,
70833
72176
  expandX: expandX,
@@ -71739,7 +73082,10 @@ const PickerCustom = props => {
71739
73082
  }
71740
73083
  return renderResolver(PickerContentInsidePopup, {
71741
73084
  ...pickerProps,
71742
- mode
73085
+ mode,
73086
+ // What a callout's own Escape means (see PickerCalloutPopup): the bubble
73087
+ // answers the key itself when the focus is in it.
73088
+ escapeEffect
71743
73089
  });
71744
73090
  };
71745
73091
 
@@ -71854,18 +73200,14 @@ const PickerContentInsidePopup = props => {
71854
73200
  calloutIcon,
71855
73201
  calloutCloseButton,
71856
73202
  calloutCloseByScroll,
71857
- popupGroup,
73203
+ escapeEffect,
71858
73204
  ...rest
71859
73205
  } = props;
71860
73206
  const isPopover = mode === "popover";
71861
73207
  const isCallout = mode === "callout";
71862
73208
  return jsx(Next, {
71863
73209
  "aria-haspopup": isPopover ? "listbox" : "dialog",
71864
- "navi-popover-mode": isPopover ? popoverMode : undefined
71865
- // On the trigger, whatever the mode: a peer's popup reads it off the
71866
- // press that lands here (see `group` in callout.js).
71867
- ,
71868
- "data-navi-popup-group": popupGroup,
73210
+ "navi-popover-mode": isPopover ? popoverMode : undefined,
71869
73211
  ...rest,
71870
73212
  // On popupProps already (see the picker's popup assembly); they mean
71871
73213
  // nothing to the picker element.
@@ -71906,7 +73248,8 @@ const PickerContentInsidePopup = props => {
71906
73248
  // nothing, so the page looks as pressable as it did before.
71907
73249
  ,
71908
73250
  backdrop: backdrop,
71909
- group: popupGroup,
73251
+ pressOutside: pressOutside,
73252
+ escapeEffect: escapeEffect,
71910
73253
  children: jsx(PopupModeContext.Provider, {
71911
73254
  value: mode,
71912
73255
  children: children
@@ -71943,7 +73286,6 @@ const PickerContentInsidePopup = props => {
71943
73286
  backdropVariant: backdropVariant,
71944
73287
  backdropColor: backdropColor,
71945
73288
  backdropFilter: backdropFilter,
71946
- group: popupGroup,
71947
73289
  focusCapture: isPopover ? focusCapture : undefined,
71948
73290
  expand: isPopover ? undefined : dialogExpand,
71949
73291
  expandX: isPopover ? undefined : dialogExpandX,
@@ -72004,7 +73346,8 @@ const PickerCalloutPopup = ({
72004
73346
  closeButton,
72005
73347
  closeByScroll,
72006
73348
  backdrop,
72007
- group,
73349
+ pressOutside,
73350
+ escapeEffect,
72008
73351
  onnavi_request_open,
72009
73352
  onnavi_request_close,
72010
73353
  onnavi_request_confirm,
@@ -72021,6 +73364,16 @@ const PickerCalloutPopup = ({
72021
73364
  anchor
72022
73365
  });
72023
73366
  const children = contentMounted ? childrenProp : null;
73367
+ // Inside a <PopupGroup> the wall is the group's (see popup_group.jsx): it
73368
+ // decides what a press outside the callout does, and the callout spends and
73369
+ // closes on none of its own.
73370
+ const popupGroup = useContext(PopupGroupContext);
73371
+ useLayoutEffect(() => {
73372
+ if (!popupGroup) {
73373
+ return undefined;
73374
+ }
73375
+ return popupGroup.register(openController);
73376
+ }, [popupGroup, openController]);
72024
73377
  // Reassigned on every render, like Popover's own, so it closes over the
72025
73378
  // latest props.
72026
73379
  openController.getElement = () => pickerRef.current;
@@ -72035,6 +73388,23 @@ const PickerCalloutPopup = ({
72035
73388
  // picker's own input — which is where the data-callout-* attributes a
72036
73389
  // caller puts on the picker land, and where the callout reads them.
72037
73390
  const anchorElement = anchor === pickerRef ? undefined : anchor && "current" in anchor ? anchor.current : anchor;
73391
+ // First, so the group already stands where it stands in the top layer
73392
+ // when the callout reads its anchor and shows itself above it.
73393
+ popupGroup?.engage({
73394
+ openController,
73395
+ // Drawn by the manager below, once the group stands.
73396
+ get popupElement() {
73397
+ return calloutManager.callout.element;
73398
+ },
73399
+ // What a data-navi-popup-inside names this popup by (aria-controls
73400
+ // names it too): the dock, not the callout drawn for it.
73401
+ popupId: id,
73402
+ pressOutside,
73403
+ event: openEvent,
73404
+ // A callout never holds the page still: a scroll is its own
73405
+ // closeByScroll's to answer.
73406
+ pageScrolls: true
73407
+ });
72038
73408
  calloutManager.addOpenToken(PICKER_CALLOUT_CONTENT_TOKEN, {
72039
73409
  message: host,
72040
73410
  // The popup a `popupTestId` names is this callout: it is the surface the
@@ -72046,8 +73416,11 @@ const PickerCalloutPopup = ({
72046
73416
  icon,
72047
73417
  closeButton,
72048
73418
  closeByScroll,
72049
- backdrop,
72050
- group,
73419
+ backdrop: popupGroup ? false : backdrop,
73420
+ closeByPressOutside: popupGroup ? false : undefined,
73421
+ // Inside the group whatever it points at: what is not a descendant of
73422
+ // an engaged group is inert, a bubble drawn there included.
73423
+ containerElement: popupGroup ? pickerEl : undefined,
72051
73424
  anchorElement,
72052
73425
  // The request, chained to the press that made it: the callout reads the
72053
73426
  // mousedown off it to wait for the release before listening for a click
@@ -72056,10 +73429,15 @@ const PickerCalloutPopup = ({
72056
73429
  // Not skipped: the callout moves the focus into the picker when it is
72057
73430
  // elsewhere, which is what lets Escape find the callout right away.
72058
73431
  skipFocus: false,
73432
+ // An Escape the callout answered itself (the focus in the bubble) means
73433
+ // what the picker's own Escape means.
72059
73434
  onClose: ({
72060
- event
73435
+ event,
73436
+ reason
72061
73437
  }) => {
72062
- openController.close(event);
73438
+ openController.close(event, {
73439
+ isCancel: reason === "escape_key" && escapeEffect === "cancel"
73440
+ });
72063
73441
  }
72064
73442
  });
72065
73443
  // A --navi-confirm said inside the callout is aimed at the callout (its
@@ -72077,9 +73455,15 @@ const PickerCalloutPopup = ({
72077
73455
  dock.setAttribute("aria-expanded", "true");
72078
73456
  return closeEvent => {
72079
73457
  calloutElement.removeEventListener("navi_request_confirm", forwardConfirm);
73458
+ if (!findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
73459
+ // Before the content goes back to the dock: a group engaged in there
73460
+ // would leave the top layer with it, its arbiter still listening.
73461
+ closePopupGroupsInside(calloutElement, closeEvent);
73462
+ }
72080
73463
  calloutManager.removeOpenToken(PICKER_CALLOUT_CONTENT_TOKEN, closeEvent);
72081
73464
  dock.appendChild(host);
72082
73465
  dock.setAttribute("aria-expanded", "false");
73466
+ popupGroup?.release(openController, closeEvent);
72083
73467
  };
72084
73468
  };
72085
73469
  return (
@@ -72107,7 +73491,10 @@ const PickerCalloutPopup = ({
72107
73491
  children: jsx("div", {
72108
73492
  ref: hostRef,
72109
73493
  "data-picker-content": "",
72110
- children: children
73494
+ children: popupGroup ? jsx(PopupGroupContext.Provider, {
73495
+ value: null,
73496
+ children: children
73497
+ }) : children
72111
73498
  })
72112
73499
  })
72113
73500
  );
@@ -73631,7 +75018,11 @@ const css$y = /* css */`@layer navi {
73631
75018
  }
73632
75019
  }
73633
75020
 
73634
- & dialog, & [popover] {
75021
+ & [popover] {
75022
+ pointer-events: auto;
75023
+ }
75024
+
75025
+ & dialog:not([navi-popup-group]:not(:modal)) {
73635
75026
  pointer-events: auto;
73636
75027
  }
73637
75028
  }
@@ -74429,7 +75820,7 @@ const css$x = /* css */`@property --list-group-label-height {
74429
75820
  background: var(--list-background-color);
74430
75821
  }
74431
75822
 
74432
- :where([popover], dialog) > .navi_list_container, .navi_list_container[popover] {
75823
+ :where([popover], dialog:not([navi-popup-group])) > .navi_list_container, .navi_list_container[popover] {
74433
75824
  --list-border-width-default: 0px;
74434
75825
  }
74435
75826
 
@@ -95830,8 +97221,9 @@ const css = /* css */`.navi_side_panel {
95830
97221
  * interaction only makes sense paired with not letting focus silently
95831
97222
  * leave the panel first. A box of the page whose press must not close the
95832
97223
  * panel (a card that fills it) names the panel:
95833
- * `data-navi-popup-inside={id}`, read when the panel has no wall
95834
- * (`backdrop={false}`) — see docs/popup_backdrop.md.
97224
+ * `data-navi-popup-inside={id}`, read when the panel has no wall of its own
97225
+ * (`backdrop={false}`, or inside a `<PopupGroup>`) — see
97226
+ * docs/popup_backdrop.md.
95835
97227
  * @param {boolean} [props.closeByDrag=true] - Pushing the panel back
95836
97228
  * towards the edge it is docked to closes it: the panel follows the
95837
97229
  * pointer and finishes leaving (or comes back to rest) when it is
@@ -96277,5 +97669,5 @@ const UserSvg = () => jsx("svg", {
96277
97669
  })
96278
97670
  });
96279
97671
 
96280
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NetworkPolicyError, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, activeRouteActionsSignal, activeRouteReadsSignal, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, dispatchRequestSetUIState, enableDebugActions, enableDebugOnDocumentLoading, enableVirtualKeyboardOverlay, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, getNowHours, getNowHoursRoundedToStep, isCellSelected, isColumnSelected, isNetworkPolicyError, isRowSelected, isScrolling, isToday, keepPageOnScreen, languagesSignal, localStorageSignal, markAsOutsideTextFlow, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, preloadUrl, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, routeFallback, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useHoldScrollRestoration, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePickerState, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
97672
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NetworkPolicyError, NumberSpin, Paragraph, Picker, Popover, Popup, PopupGroup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, activeRouteActionsSignal, activeRouteReadsSignal, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, dispatchRequestSetUIState, enableDebugActions, enableDebugOnDocumentLoading, enableVirtualKeyboardOverlay, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, getNowHours, getNowHoursRoundedToStep, isCellSelected, isColumnSelected, isNetworkPolicyError, isRowSelected, isScrolling, isToday, keepPageOnScreen, languagesSignal, localStorageSignal, markAsOutsideTextFlow, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, preloadUrl, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, routeFallback, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useHoldScrollRestoration, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePickerState, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
96281
97673
  //# sourceMappingURL=jsenv_navi.js.map