@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.
- package/README.md +1 -1
- package/dist/dev/jsenv_navi.js +2663 -1267
- package/dist/dev/jsenv_navi.js.map +20 -18
- package/dist/jsenv_navi.js +2119 -727
- package/dist/jsenv_navi.js.map +20 -18
- package/docs/AI_INSTRUCTIONS.md +5 -3
- package/docs/popup_backdrop.md +126 -9
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -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,
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
17766
|
+
const holder = containerElement || anchorElement;
|
|
17767
|
+
if (!holder || holder === document.body) {
|
|
18249
17768
|
return document.body;
|
|
18250
17769
|
}
|
|
18251
|
-
const closestLabel =
|
|
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(
|
|
18264
|
-
return
|
|
17782
|
+
if (canContainCallout(holder)) {
|
|
17783
|
+
return holder;
|
|
18265
17784
|
}
|
|
18266
|
-
return
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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
|
|
33962
|
-
// ::backdrop, and a popover's, which is
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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 (
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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.
|
|
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
|
|
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) &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69720
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
&
|
|
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}
|
|
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
|