@jsenv/navi 0.29.464 → 0.29.466
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 +2782 -1291
- package/dist/dev/jsenv_navi.js.map +23 -21
- package/dist/jsenv_navi.js +2236 -749
- package/dist/jsenv_navi.js.map +23 -21
- 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, overflowGoesToViewport, 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
|
}
|
|
@@ -6554,6 +6554,12 @@ const isAimedAtSelfInteractionsBelow = (
|
|
|
6554
6554
|
if (!target || typeof target.closest !== "function") {
|
|
6555
6555
|
return false;
|
|
6556
6556
|
}
|
|
6557
|
+
if (event.type === "keydown" || event.type === "keyup") {
|
|
6558
|
+
// A key is no press: the claim is the pointer's. An Escape pressed while
|
|
6559
|
+
// the focus rests on the claimer asks the controls around it — the sheet
|
|
6560
|
+
// it sits in closing.
|
|
6561
|
+
return false;
|
|
6562
|
+
}
|
|
6557
6563
|
const claimer = target.closest(CLICK_SELECTOR);
|
|
6558
6564
|
if (!claimer) {
|
|
6559
6565
|
return false;
|
|
@@ -15838,6 +15844,48 @@ const createOpenController = (
|
|
|
15838
15844
|
// armSuppressNextOpenRequest below.
|
|
15839
15845
|
let suppressNextOpenRequest = false;
|
|
15840
15846
|
let disarmSuppressNextOpenRequest = null;
|
|
15847
|
+
// The press a press-close came from, for as long as that press lasts (see
|
|
15848
|
+
// open() for what it is read for).
|
|
15849
|
+
let pressCountAtPressClose = null;
|
|
15850
|
+
let releaseThisPress = null;
|
|
15851
|
+
const holdReopenForThisPress = (closeEvent) => {
|
|
15852
|
+
releaseThisPress?.();
|
|
15853
|
+
pressCountAtPressClose = pressCount;
|
|
15854
|
+
let timeout = null;
|
|
15855
|
+
const release = () => {
|
|
15856
|
+
clearTimeout(timeout);
|
|
15857
|
+
document.removeEventListener("click", onPressEnd, { capture: true });
|
|
15858
|
+
document.removeEventListener("pointercancel", onPressEnd, {
|
|
15859
|
+
capture: true,
|
|
15860
|
+
});
|
|
15861
|
+
document.removeEventListener("pointerdown", release, { capture: true });
|
|
15862
|
+
pressCountAtPressClose = null;
|
|
15863
|
+
releaseThisPress = null;
|
|
15864
|
+
};
|
|
15865
|
+
// Once the task the press ends in is over: every reaction to the closing
|
|
15866
|
+
// press is synchronous to it, while an open asked for later — a command a
|
|
15867
|
+
// button runs once its action resolves — is somebody asking again.
|
|
15868
|
+
const onPressEnd = () => {
|
|
15869
|
+
document.removeEventListener("click", onPressEnd, { capture: true });
|
|
15870
|
+
document.removeEventListener("pointercancel", onPressEnd, {
|
|
15871
|
+
capture: true,
|
|
15872
|
+
});
|
|
15873
|
+
timeout = setTimeout(release);
|
|
15874
|
+
};
|
|
15875
|
+
if (findEvent(closeEvent, "click")) {
|
|
15876
|
+
// Closed by the press's own click: a listener added during its dispatch
|
|
15877
|
+
// would only hear the next one.
|
|
15878
|
+
timeout = setTimeout(release);
|
|
15879
|
+
} else {
|
|
15880
|
+
document.addEventListener("click", onPressEnd, { capture: true });
|
|
15881
|
+
document.addEventListener("pointercancel", onPressEnd, {
|
|
15882
|
+
capture: true,
|
|
15883
|
+
});
|
|
15884
|
+
}
|
|
15885
|
+
// A press whose click was swallowed (a gesture) ends with the next one.
|
|
15886
|
+
document.addEventListener("pointerdown", release, { capture: true });
|
|
15887
|
+
releaseThisPress = release;
|
|
15888
|
+
};
|
|
15841
15889
|
|
|
15842
15890
|
// When the popup closes because of a press (clicking the backdrop, or a
|
|
15843
15891
|
// press outside a popup with no backdrop), the browser still dispatches the
|
|
@@ -15993,6 +16041,7 @@ const createOpenController = (
|
|
|
15993
16041
|
`closed by ${pressEvent.type} -> ignore next click`,
|
|
15994
16042
|
);
|
|
15995
16043
|
armSuppressNextOpenRequest();
|
|
16044
|
+
holdReopenForThisPress(closeEvent);
|
|
15996
16045
|
break prevent_reopen;
|
|
15997
16046
|
}
|
|
15998
16047
|
|
|
@@ -16077,6 +16126,17 @@ const createOpenController = (
|
|
|
16077
16126
|
{ opened: false, event: closeEvent },
|
|
16078
16127
|
);
|
|
16079
16128
|
};
|
|
16129
|
+
// A cancel an Escape asked for, read with what Escape means for this popup.
|
|
16130
|
+
const readCloseDetail = (e, detail) => {
|
|
16131
|
+
if (
|
|
16132
|
+
detail?.isCancel &&
|
|
16133
|
+
controller.escapeEffect !== "cancel" &&
|
|
16134
|
+
findEvent(e, isEscapeKeydown)
|
|
16135
|
+
) {
|
|
16136
|
+
return { ...detail, isCancel: false };
|
|
16137
|
+
}
|
|
16138
|
+
return detail;
|
|
16139
|
+
};
|
|
16080
16140
|
const controller = {
|
|
16081
16141
|
opened: false,
|
|
16082
16142
|
// What the DOM currently shows, as opposed to what the controller has
|
|
@@ -16108,6 +16168,11 @@ const createOpenController = (
|
|
|
16108
16168
|
// whole point: learning it afterwards means the content mounted on the
|
|
16109
16169
|
// previous subject first.
|
|
16110
16170
|
onOpen: null,
|
|
16171
|
+
// What an Escape means for this popup, whoever heard the key — its own
|
|
16172
|
+
// shortcut, its trigger's, the group it stands in, a bubble's: a cancel
|
|
16173
|
+
// (what it holds reverts), unless its owner says otherwise (a Picker's
|
|
16174
|
+
// escapeEffect="close"). Read from the close the key asked for.
|
|
16175
|
+
escapeEffect: "cancel",
|
|
16111
16176
|
// The counterpart, set only when the popup was told to throw its content
|
|
16112
16177
|
// away on close (`mount="while-opened"`). Called from performClose above.
|
|
16113
16178
|
unmountContent: null,
|
|
@@ -16127,7 +16192,9 @@ const createOpenController = (
|
|
|
16127
16192
|
// called once the open/close has fully happened rather than mid-sequence.
|
|
16128
16193
|
onOpenedChange: null,
|
|
16129
16194
|
open: (e, detail) => {
|
|
16130
|
-
|
|
16195
|
+
// The refusals below read the decision (`opened`), which is written
|
|
16196
|
+
// before a change waiting for its view transition runs: a refused open
|
|
16197
|
+
// must not force that change through and take its picture away.
|
|
16131
16198
|
if (controller.opened || !controller.openEffect) {
|
|
16132
16199
|
return;
|
|
16133
16200
|
}
|
|
@@ -16135,6 +16202,20 @@ const createOpenController = (
|
|
|
16135
16202
|
suppressNextOpenRequest = false;
|
|
16136
16203
|
return;
|
|
16137
16204
|
}
|
|
16205
|
+
if (
|
|
16206
|
+
pressCountAtPressClose === pressCount &&
|
|
16207
|
+
findEvent(e, isEventOfAPress)
|
|
16208
|
+
) {
|
|
16209
|
+
// The press that closed it, heard again by what opens it. With nothing
|
|
16210
|
+
// between a popup and its own trigger (no wall, or a PopupGroup), the
|
|
16211
|
+
// press is still on its way when the popup closes, and the trigger's
|
|
16212
|
+
// own reaction to that same press asks to open it again: at its
|
|
16213
|
+
// mousedown, before the click the suppression above waits for — or at
|
|
16214
|
+
// the very click that closed it, whose suppression is armed during its
|
|
16215
|
+
// own dispatch and so only hears the next one.
|
|
16216
|
+
return;
|
|
16217
|
+
}
|
|
16218
|
+
flushChangeAwaitingTransition();
|
|
16138
16219
|
const requestOpenEvent = new CustomEvent("navi_request_open", {
|
|
16139
16220
|
detail: { event: e, ...detail },
|
|
16140
16221
|
cancelable: true,
|
|
@@ -16261,11 +16342,20 @@ const createOpenController = (
|
|
|
16261
16342
|
return true;
|
|
16262
16343
|
}
|
|
16263
16344
|
const requestCloseEvent = new CustomEvent("navi_request_close", {
|
|
16264
|
-
detail: { event: e, ...detail },
|
|
16345
|
+
detail: { event: e, ...readCloseDetail(e, detail) },
|
|
16265
16346
|
cancelable: true,
|
|
16266
16347
|
});
|
|
16267
16348
|
chainEvent(requestCloseEvent, e);
|
|
16268
16349
|
closeHandlers?.onRequestClose?.(requestCloseEvent);
|
|
16350
|
+
// An Escape this popup answered, by closing or by refusing: dispatched
|
|
16351
|
+
// on, it would also be the browser's close request for a modal around
|
|
16352
|
+
// the popup (a PopupGroup, a dialog), closing what this popup kept open,
|
|
16353
|
+
// or what is still closing with it (a lifting dialog closes a frame
|
|
16354
|
+
// later).
|
|
16355
|
+
const escapeKeydown = findEvent(requestCloseEvent, isEscapeKeydown);
|
|
16356
|
+
if (escapeKeydown) {
|
|
16357
|
+
escapeKeydown.preventDefault();
|
|
16358
|
+
}
|
|
16269
16359
|
if (requestCloseEvent.defaultPrevented) {
|
|
16270
16360
|
// The native <dialog> "cancel" event (Escape key) closes the dialog
|
|
16271
16361
|
// by default; prevent that default so denial actually keeps it open.
|
|
@@ -16284,7 +16374,7 @@ const createOpenController = (
|
|
|
16284
16374
|
return;
|
|
16285
16375
|
}
|
|
16286
16376
|
const closeEvent = new CustomEvent("navi_close", {
|
|
16287
|
-
detail: { event: e, ...detail },
|
|
16377
|
+
detail: { event: e, ...readCloseDetail(e, detail) },
|
|
16288
16378
|
});
|
|
16289
16379
|
chainEvent(closeEvent, e);
|
|
16290
16380
|
// Skips onRequestClose entirely — there is no choice here.
|
|
@@ -16294,13 +16384,27 @@ const createOpenController = (
|
|
|
16294
16384
|
return controller;
|
|
16295
16385
|
};
|
|
16296
16386
|
|
|
16387
|
+
// A click a keyboard made (detail 0) is not one: nothing pressed for it.
|
|
16388
|
+
const isEscapeKeydown = (event) =>
|
|
16389
|
+
event.type === "keydown" && event.key === "Escape";
|
|
16390
|
+
|
|
16391
|
+
const isEventOfAPress = (event) =>
|
|
16392
|
+
event.type === "pointerdown" ||
|
|
16393
|
+
event.type === "mousedown" ||
|
|
16394
|
+
(event.type === "click" && event.detail > 0);
|
|
16395
|
+
|
|
16297
16396
|
// Inside a popup that is open right now — the popup being closed, in practice,
|
|
16298
16397
|
// since that is the one the key press was delivered to.
|
|
16299
16398
|
const isInsideOpenPopup = (element) => {
|
|
16300
16399
|
if (!element || element.nodeType !== 1) {
|
|
16301
16400
|
return false;
|
|
16302
16401
|
}
|
|
16303
|
-
|
|
16402
|
+
// An engaged PopupGroup is open too, around peers that are not the popup.
|
|
16403
|
+
return Boolean(
|
|
16404
|
+
element.closest(
|
|
16405
|
+
"dialog[open]:not([navi-popup-group]), [popover]:popover-open",
|
|
16406
|
+
),
|
|
16407
|
+
);
|
|
16304
16408
|
};
|
|
16305
16409
|
|
|
16306
16410
|
// What Enter is about to do when it is about to activate something: press the
|
|
@@ -16766,572 +16870,6 @@ const useOpenControllerByProps = (props, name) => {
|
|
|
16766
16870
|
return openController;
|
|
16767
16871
|
};
|
|
16768
16872
|
|
|
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
16873
|
/**
|
|
17336
16874
|
* Typography reset shared by the surfaces a control opens: callout, popover,
|
|
17337
16875
|
* dialog.
|
|
@@ -17454,7 +16992,7 @@ installImportMetaCssBuild(import.meta);
|
|
|
17454
16992
|
* (see calloutTemplate in callout.js).
|
|
17455
16993
|
*/
|
|
17456
16994
|
|
|
17457
|
-
const css$
|
|
16995
|
+
const css$16 = /* css */`.navi_callout_status_icon {
|
|
17458
16996
|
--x-callout-status-icon-color: var(--navi-callout-neutral-color);
|
|
17459
16997
|
box-sizing: border-box;
|
|
17460
16998
|
aspect-ratio: 1;
|
|
@@ -17525,7 +17063,7 @@ const CalloutStatusIcon = ({
|
|
|
17525
17063
|
status = "info",
|
|
17526
17064
|
shape = "square"
|
|
17527
17065
|
}) => {
|
|
17528
|
-
import.meta.css = [css$
|
|
17066
|
+
import.meta.css = [css$16, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
|
|
17529
17067
|
return jsx("span", {
|
|
17530
17068
|
className: "navi_callout_status_icon",
|
|
17531
17069
|
"data-status": status === "none" ? undefined : status,
|
|
@@ -17566,11 +17104,13 @@ const SPENT_PRESS_EVENT_TYPES = [
|
|
|
17566
17104
|
"contextmenu",
|
|
17567
17105
|
];
|
|
17568
17106
|
// 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.
|
|
17107
|
+
// itself: a popup of navi's (either layer) or a native top-layer element. An
|
|
17108
|
+
// idle PopupGroup is a closed <dialog> that is part of the page (see
|
|
17109
|
+
// popup_group.jsx).
|
|
17570
17110
|
const LAYER_SELECTOR =
|
|
17571
|
-
'[popover], dialog, [navi-control="popover"], [navi-control="dialog"]';
|
|
17111
|
+
'[popover], dialog:not([navi-popup-group]:not(:modal)), [navi-control="popover"], [navi-control="dialog"]';
|
|
17572
17112
|
|
|
17573
|
-
const css$
|
|
17113
|
+
const css$15 = /* css */ `
|
|
17574
17114
|
/* jsenv-css-opaque: same as dialog.jsx, cascade order. */
|
|
17575
17115
|
@layer navi {
|
|
17576
17116
|
.navi_callout {
|
|
@@ -17843,6 +17383,10 @@ const css$14 = /* css */ `
|
|
|
17843
17383
|
* If not provided or too big, callout will be centered in viewport. It says where the
|
|
17844
17384
|
* callout is drawn, not what dismisses it: see `openingEvent` for what a container anchor
|
|
17845
17385
|
* costs
|
|
17386
|
+
* @param {HTMLElement} [options.containerElement] - Where the callout element goes in the
|
|
17387
|
+
* DOM, when that is not where the anchor puts it (the anchor itself, or beside it). What
|
|
17388
|
+
* holds a callout decides what it inherits and whether it can be reached at all: outside
|
|
17389
|
+
* an engaged PopupGroup it is inert. The anchor still says where it points
|
|
17846
17390
|
* @param {string} [options.status=""] - Callout status: "info" | "warning" | "error" | "success"
|
|
17847
17391
|
* @param {string} [options.testId] - `data-testid` on the callout element. The callout is
|
|
17848
17392
|
* drawn by navi, so nothing the caller renders can carry the name a test needs — same
|
|
@@ -17864,10 +17408,6 @@ const css$14 = /* css */ `
|
|
|
17864
17408
|
* still scrolls, a press on a layer drawn over the page (a sheet the callout's content
|
|
17865
17409
|
* opened) is that layer's, and a press that does not close the callout (an "error" one) is
|
|
17866
17410
|
* 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
17411
|
* @param {Event} [options.openingEvent] - The gesture the callout belongs to. It names the
|
|
17872
17412
|
* opener — `currentTarget` while the event is still dispatching, `target` once it is over,
|
|
17873
17413
|
* so awaiting before opening changes nothing — and the opener is the one part of the anchor
|
|
@@ -17928,12 +17468,12 @@ const openCallout = (
|
|
|
17928
17468
|
// "" - neutral information
|
|
17929
17469
|
status = "",
|
|
17930
17470
|
testId,
|
|
17471
|
+
containerElement,
|
|
17931
17472
|
onClose,
|
|
17932
17473
|
closeByPressOutside = status === "info",
|
|
17933
17474
|
closeOnFocusLeave = closeByPressOutside,
|
|
17934
17475
|
closeByScroll = false,
|
|
17935
17476
|
backdrop = false,
|
|
17936
|
-
group,
|
|
17937
17477
|
closeButton = true,
|
|
17938
17478
|
openingEvent,
|
|
17939
17479
|
reopen = "toggle",
|
|
@@ -17943,7 +17483,7 @@ const openCallout = (
|
|
|
17943
17483
|
debug = () => {},
|
|
17944
17484
|
} = {},
|
|
17945
17485
|
) => {
|
|
17946
|
-
import.meta.css = [css$
|
|
17486
|
+
import.meta.css = [css$15, "@jsenv/navi/src/control/rules/callout/callout.js"];
|
|
17947
17487
|
if (debug === true) {
|
|
17948
17488
|
debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
|
|
17949
17489
|
}
|
|
@@ -18245,10 +17785,11 @@ const openCallout = (
|
|
|
18245
17785
|
}
|
|
18246
17786
|
}
|
|
18247
17787
|
const calloutContainer = (() => {
|
|
18248
|
-
|
|
17788
|
+
const holder = containerElement || anchorElement;
|
|
17789
|
+
if (!holder || holder === document.body) {
|
|
18249
17790
|
return document.body;
|
|
18250
17791
|
}
|
|
18251
|
-
const closestLabel =
|
|
17792
|
+
const closestLabel = holder.closest("label");
|
|
18252
17793
|
if (closestLabel) {
|
|
18253
17794
|
// Putting callout inside label would be problematic:
|
|
18254
17795
|
// - Click on callout would be catched by the browser and re-dispatched to the label
|
|
@@ -18260,10 +17801,10 @@ const openCallout = (
|
|
|
18260
17801
|
return labelParent;
|
|
18261
17802
|
}
|
|
18262
17803
|
// Some elements (e.g. <input>) cannot have children
|
|
18263
|
-
if (canContainCallout(
|
|
18264
|
-
return
|
|
17804
|
+
if (canContainCallout(holder)) {
|
|
17805
|
+
return holder;
|
|
18265
17806
|
}
|
|
18266
|
-
return
|
|
17807
|
+
return holder.parentNode || document.body;
|
|
18267
17808
|
})();
|
|
18268
17809
|
|
|
18269
17810
|
{
|
|
@@ -18310,8 +17851,7 @@ const openCallout = (
|
|
|
18310
17851
|
// Whether an outside press on `target` is spent on closing the callout
|
|
18311
17852
|
// (`backdrop`). What is not the page behind the callout keeps its press: a
|
|
18312
17853
|
// 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.
|
|
17854
|
+
// sheet its content opened is in front of it.
|
|
18315
17855
|
const spendsPressOn = (target) => {
|
|
18316
17856
|
if (!backdrop || !pressOutsideCloses()) {
|
|
18317
17857
|
return false;
|
|
@@ -18320,9 +17860,6 @@ const openCallout = (
|
|
|
18320
17860
|
if (layer && !layer.contains(calloutElement)) {
|
|
18321
17861
|
return false;
|
|
18322
17862
|
}
|
|
18323
|
-
if (findPopupPeer(target, group)) {
|
|
18324
|
-
return false;
|
|
18325
|
-
}
|
|
18326
17863
|
return true;
|
|
18327
17864
|
};
|
|
18328
17865
|
const spend = (event) => {
|
|
@@ -18491,8 +18028,10 @@ const openCallout = (
|
|
|
18491
18028
|
}
|
|
18492
18029
|
calloutCloseButton.tabIndex = -1;
|
|
18493
18030
|
calloutCloseButton.setAttribute("navi-focus-delegate", "");
|
|
18031
|
+
// One Escape closes one popup: an Escape already spent by one in front
|
|
18032
|
+
// of the callout — a menu opened from its content — is not this one's.
|
|
18494
18033
|
const onAnchorKeydown = (e) => {
|
|
18495
|
-
if (e.key === "Escape") {
|
|
18034
|
+
if (e.key === "Escape" && !e.defaultPrevented) {
|
|
18496
18035
|
requestClose(e, "escape_key");
|
|
18497
18036
|
e.stopPropagation();
|
|
18498
18037
|
e.preventDefault();
|
|
@@ -18507,7 +18046,7 @@ const openCallout = (
|
|
|
18507
18046
|
// Usually callout is inside anchor so the anchor would see the escape being pressed
|
|
18508
18047
|
// but callout can be appended outside anchor when anchor cannot receive children
|
|
18509
18048
|
const onCalloutKeydown = (e) => {
|
|
18510
|
-
if (e.key === "Escape") {
|
|
18049
|
+
if (e.key === "Escape" && !e.defaultPrevented) {
|
|
18511
18050
|
requestClose(e, "escape_key");
|
|
18512
18051
|
e.stopPropagation();
|
|
18513
18052
|
e.preventDefault();
|
|
@@ -18553,21 +18092,23 @@ const openCallout = (
|
|
|
18553
18092
|
//
|
|
18554
18093
|
// Only for the ones that cannot be heard otherwise, and only while they are
|
|
18555
18094
|
// open: a callout the anchor can hear about is left to the anchor, so
|
|
18556
|
-
// Escape goes on meaning what it means where the user is.
|
|
18095
|
+
// Escape goes on meaning what it means where the user is. Heard once the
|
|
18096
|
+
// key has been through what it was pressed in: one that answered it (a
|
|
18097
|
+
// popup closing, a field clearing) spent it.
|
|
18557
18098
|
if (anchorElement && anchorElement.contains(document.activeElement)) {
|
|
18558
18099
|
break close_on_escape_from_anywhere;
|
|
18559
18100
|
}
|
|
18560
18101
|
const onDocumentKeydown = (e) => {
|
|
18561
|
-
if (e.key !== "Escape") {
|
|
18102
|
+
if (e.key !== "Escape" || e.defaultPrevented) {
|
|
18562
18103
|
return;
|
|
18563
18104
|
}
|
|
18564
18105
|
requestClose(e, "escape_key");
|
|
18565
18106
|
e.stopPropagation();
|
|
18566
18107
|
e.preventDefault();
|
|
18567
18108
|
};
|
|
18568
|
-
document.addEventListener("keydown", onDocumentKeydown
|
|
18109
|
+
document.addEventListener("keydown", onDocumentKeydown);
|
|
18569
18110
|
addTeardown(() => {
|
|
18570
|
-
document.removeEventListener("keydown", onDocumentKeydown
|
|
18111
|
+
document.removeEventListener("keydown", onDocumentKeydown);
|
|
18571
18112
|
});
|
|
18572
18113
|
}
|
|
18573
18114
|
{
|
|
@@ -19538,7 +19079,7 @@ const generateSvgWithoutArrow = (width, height) => {
|
|
|
19538
19079
|
*
|
|
19539
19080
|
* Usage:
|
|
19540
19081
|
* const myToken = createOpenToken();
|
|
19541
|
-
* calloutManager.addOpenToken(myToken, { message, status, testId, closeByScroll, backdrop,
|
|
19082
|
+
* calloutManager.addOpenToken(myToken, { message, status, testId, closeByScroll, backdrop, closeByPressOutside, containerElement, anchorElement, event, skipFocus, onClose });
|
|
19542
19083
|
* calloutManager.removeOpenToken(myToken, event);
|
|
19543
19084
|
* calloutManager.requestCloseCallout(event, debugReason); // force-close all
|
|
19544
19085
|
* calloutManager.callout // current open callout or null
|
|
@@ -19594,10 +19135,15 @@ const createCalloutManager = (
|
|
|
19594
19135
|
testId: tokenData.testId,
|
|
19595
19136
|
icon: tokenData.icon,
|
|
19596
19137
|
closeButton: tokenData.closeButton,
|
|
19597
|
-
|
|
19138
|
+
// Unless the token says otherwise: what a press outside a picker's
|
|
19139
|
+
// callout does inside a PopupGroup is the group's to decide. Focus
|
|
19140
|
+
// leaving stays the callout's own way out, whoever answers the press.
|
|
19141
|
+
closeByPressOutside:
|
|
19142
|
+
tokenData.closeByPressOutside ?? tokenData.status !== "error",
|
|
19143
|
+
closeOnFocusLeave: tokenData.status !== "error",
|
|
19144
|
+
containerElement: tokenData.containerElement,
|
|
19598
19145
|
closeByScroll: tokenData.closeByScroll,
|
|
19599
19146
|
backdrop: tokenData.backdrop,
|
|
19600
|
-
group: tokenData.group,
|
|
19601
19147
|
anchorElement,
|
|
19602
19148
|
openingEvent: event,
|
|
19603
19149
|
skipFocus: tokenData.skipFocus,
|
|
@@ -19727,7 +19273,8 @@ const createCalloutManager = (
|
|
|
19727
19273
|
closeButton,
|
|
19728
19274
|
closeByScroll,
|
|
19729
19275
|
backdrop,
|
|
19730
|
-
|
|
19276
|
+
closeByPressOutside,
|
|
19277
|
+
containerElement,
|
|
19731
19278
|
anchorElement,
|
|
19732
19279
|
event,
|
|
19733
19280
|
skipFocus,
|
|
@@ -19746,7 +19293,8 @@ const createCalloutManager = (
|
|
|
19746
19293
|
closeButton,
|
|
19747
19294
|
closeByScroll,
|
|
19748
19295
|
backdrop,
|
|
19749
|
-
|
|
19296
|
+
closeByPressOutside,
|
|
19297
|
+
containerElement,
|
|
19750
19298
|
skipFocus,
|
|
19751
19299
|
onClose,
|
|
19752
19300
|
// Resolved as the token is added, and kept with it: a token shown later
|
|
@@ -22066,7 +21614,10 @@ const readNumberFromDom = (element, attribute, defaultValue) => {
|
|
|
22066
21614
|
* what a popup outside the top layer says for itself).
|
|
22067
21615
|
*/
|
|
22068
21616
|
|
|
22069
|
-
|
|
21617
|
+
// An idle PopupGroup is a closed <dialog> that is part of the page, not a layer
|
|
21618
|
+
// over it (see popup_group.jsx).
|
|
21619
|
+
const LAYER_OVER_SELECTOR =
|
|
21620
|
+
"[data-drag-ignore],[popover],dialog:not([navi-popup-group]:not(:modal))";
|
|
22070
21621
|
|
|
22071
21622
|
/**
|
|
22072
21623
|
* The nearest word wins: a layer INSIDE the element takes the press away from
|
|
@@ -22245,7 +21796,7 @@ import.meta.css = /* css */ [`@property --swipe-progress {
|
|
|
22245
21796
|
user-select: none;
|
|
22246
21797
|
}
|
|
22247
21798
|
|
|
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"]) {
|
|
21799
|
+
: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
21800
|
user-select: text;
|
|
22250
21801
|
}
|
|
22251
21802
|
|
|
@@ -33184,7 +32735,7 @@ const keepLeavingPages = () => {
|
|
|
33184
32735
|
};
|
|
33185
32736
|
};
|
|
33186
32737
|
const LEAVING_ATTRIBUTE = "data-navi-route-leaving";
|
|
33187
|
-
const css$
|
|
32738
|
+
const css$14 = /* css */`[data-navi-route-leaving] {
|
|
33188
32739
|
display: none !important;
|
|
33189
32740
|
}
|
|
33190
32741
|
`;
|
|
@@ -33456,7 +33007,7 @@ const useContentKeepingLeavingPages = (branch, readShownBranch, parentPage) => {
|
|
|
33456
33007
|
if (leaving.length === 0) {
|
|
33457
33008
|
return vnode ? [vnode] : null;
|
|
33458
33009
|
}
|
|
33459
|
-
import.meta.css = [css$
|
|
33010
|
+
import.meta.css = [css$14, "@jsenv/navi/src/nav/route.jsx"];
|
|
33460
33011
|
const children = vnode ? [vnode] : [];
|
|
33461
33012
|
for (const entry of leaving) {
|
|
33462
33013
|
children.push(h("template", {
|
|
@@ -33951,20 +33502,21 @@ const TOP_LAYER_SELECTOR = ":modal, :popover-open";
|
|
|
33951
33502
|
// stands over the pages in the top layer. A sticky row inside the pages needs
|
|
33952
33503
|
// none of this — it lives in the area, so it is already part of the pages' own
|
|
33953
33504
|
// 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})`;
|
|
33505
|
+
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
33506
|
// Worn by the root for the length of a route transition (route_transition.jsx
|
|
33956
33507
|
// owns it). Written out rather than imported: importing the module that owns
|
|
33957
33508
|
// it back into this one would close a cycle.
|
|
33958
33509
|
const TRANSITION_ATTRIBUTE$1 = "data-navi-route-transition";
|
|
33959
33510
|
const NAME_PROPERTY$1 = "view-transition-name";
|
|
33960
33511
|
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
|
|
33512
|
+
// The walls painted in the top layer: a modal dialog's (a PopupGroup's too,
|
|
33513
|
+
// layout/popup_group.jsx), which is its ::backdrop, and a popover's, which is
|
|
33514
|
+
// a real element shown as a popover
|
|
33963
33515
|
// beside it (layout/popover.jsx) — unnamed, so covered by the pictures for
|
|
33964
33516
|
// the length of the movement all the same. A top-layer dialog without a wall
|
|
33965
33517
|
// is shown as a popover and has nothing to stand in for.
|
|
33966
33518
|
const TOP_LAYER_WALL_SELECTOR =
|
|
33967
|
-
".navi_dialog:modal, .navi_popover_backdrop:popover-open";
|
|
33519
|
+
".navi_dialog:modal, .navi_popover_backdrop:popover-open, .navi_popup_group:modal";
|
|
33968
33520
|
const WALL_ATTRIBUTE = "data-navi-transition-wall";
|
|
33969
33521
|
// Which side of the movement a bar is on, worn by the bar and read by the
|
|
33970
33522
|
// browser as the class of its group (the CSS below): "leaving" from the first
|
|
@@ -34026,7 +33578,7 @@ const TRANSITION_FURNITURE_CSS = /* css */ `.navi_fixed_bar[data-navi-transition
|
|
|
34026
33578
|
}
|
|
34027
33579
|
|
|
34028
33580
|
:root[data-navi-route-transition-target="area"] {
|
|
34029
|
-
& .navi_dialog::backdrop, & .navi_popover_backdrop {
|
|
33581
|
+
& .navi_dialog::backdrop, & .navi_popup_group::backdrop, & .navi_popover_backdrop {
|
|
34030
33582
|
backdrop-filter: none;
|
|
34031
33583
|
background: none;
|
|
34032
33584
|
}
|
|
@@ -34828,7 +34380,7 @@ const readOuterScroll = (element) => {
|
|
|
34828
34380
|
};
|
|
34829
34381
|
|
|
34830
34382
|
const isDocumentScroller = (node) => {
|
|
34831
|
-
return node === document.documentElement
|
|
34383
|
+
return node === document.documentElement;
|
|
34832
34384
|
};
|
|
34833
34385
|
|
|
34834
34386
|
const isPinnedToViewport = (node) => {
|
|
@@ -35127,7 +34679,7 @@ const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
|
|
|
35127
34679
|
// the root pictures must NOT move (they carry the whole viewport, blank bands
|
|
35128
34680
|
// included).
|
|
35129
34681
|
|
|
35130
|
-
const css$
|
|
34682
|
+
const css$13 = /* css */`@property --navi-route-transition-leave {
|
|
35131
34683
|
syntax: "*";
|
|
35132
34684
|
inherits: false
|
|
35133
34685
|
}
|
|
@@ -36709,7 +36261,7 @@ const describePage = ({
|
|
|
36709
36261
|
// is the application's element as often as <RouteTransitionArea>, and is
|
|
36710
36262
|
// only found as a movement starts.
|
|
36711
36263
|
const installRouteTransitionCss = () => {
|
|
36712
|
-
import.meta.css = [css$
|
|
36264
|
+
import.meta.css = [css$13, "@jsenv/navi/src/nav/route_transition.jsx"];
|
|
36713
36265
|
installTransitionWindowCss();
|
|
36714
36266
|
installTransitionFurnitureCss();
|
|
36715
36267
|
};
|
|
@@ -36746,7 +36298,7 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
|
|
|
36746
36298
|
const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
|
|
36747
36299
|
// The name the box wears while it travels, and only then (see nameForTravel).
|
|
36748
36300
|
const TRAVEL_NAME = "navi-route-travel";
|
|
36749
|
-
const css$
|
|
36301
|
+
const css$12 = /* css */`.navi_route_travel {
|
|
36750
36302
|
position: relative;
|
|
36751
36303
|
|
|
36752
36304
|
&[data-travel-by-drag="x"] {
|
|
@@ -37044,7 +36596,7 @@ const RouteTravel = ({
|
|
|
37044
36596
|
children,
|
|
37045
36597
|
...rest
|
|
37046
36598
|
}) => {
|
|
37047
|
-
import.meta.css = [css$
|
|
36599
|
+
import.meta.css = [css$12, "@jsenv/navi/src/nav/route_travel.jsx"];
|
|
37048
36600
|
installTransitionWindowCss();
|
|
37049
36601
|
const elementRef = useRef();
|
|
37050
36602
|
const gestureRef = useRef(null);
|
|
@@ -38054,12 +37606,12 @@ const whilePageRenders = async (page, change, wait = armRouteRenderWait()) => {
|
|
|
38054
37606
|
// (`overflow: clip`) holds nothing and is walked straight through — see
|
|
38055
37607
|
// is_scrollable.js.
|
|
38056
37608
|
const pagesScrollTheDocument = element => {
|
|
38057
|
-
|
|
37609
|
+
// What window.scrollTo moves. A body whose overflow is the viewport's is
|
|
37610
|
+
// walked through to it (see overflowGoesToViewport in @jsenv/dom); a body
|
|
37611
|
+
// the walk stops on scrolls itself, out of the viewport's reach.
|
|
37612
|
+
return getScrollContainer(element, {
|
|
38058
37613
|
includeHidden: true
|
|
38059
|
-
});
|
|
38060
|
-
// html and body are one answer: whichever of them the walk stops on, what
|
|
38061
|
-
// scrolls is the viewport, which is what window.scrollTo moves.
|
|
38062
|
-
return scrollContainer === document.documentElement || scrollContainer === document.body;
|
|
37614
|
+
}) === document.documentElement;
|
|
38063
37615
|
};
|
|
38064
37616
|
|
|
38065
37617
|
// A page of the row: a route, and the params that say which of its tabs when
|
|
@@ -40444,6 +39996,504 @@ const NAVI_COMMANDS = {
|
|
|
40444
39996
|
"--navi-unselect": unselectCommand,
|
|
40445
39997
|
};
|
|
40446
39998
|
|
|
39999
|
+
/**
|
|
40000
|
+
* Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
|
|
40001
|
+
* (non-top-layer) renderers — operate on a plain DOM element, no knowledge
|
|
40002
|
+
* of which of the two owns it.
|
|
40003
|
+
*/
|
|
40004
|
+
|
|
40005
|
+
|
|
40006
|
+
/**
|
|
40007
|
+
* Whether a visibleRectEffect delivery is one that can have taken height away
|
|
40008
|
+
* from a popup, and so pushed whatever holds focus out of sight:
|
|
40009
|
+
* - "resize": the window/visual viewport settled — which is also how the
|
|
40010
|
+
* on-screen keyboard arrives, overlay or not (window_size.js);
|
|
40011
|
+
* - ELEMENT_SIZE_CHANGE: the popup's own box measured different;
|
|
40012
|
+
* - "focusin": the focus-settled re-measure, for the room that changes with
|
|
40013
|
+
* nothing announcing it (subscribeFocusSettled in window_size.js).
|
|
40014
|
+
*
|
|
40015
|
+
* Everything else is a scroll of one kind or another, where nothing got
|
|
40016
|
+
* smaller and scrolling the focused element back would fight the very gesture
|
|
40017
|
+
* that fired it.
|
|
40018
|
+
*/
|
|
40019
|
+
const mayHaveHiddenFocus = (event) => {
|
|
40020
|
+
const type = event?.type;
|
|
40021
|
+
return (
|
|
40022
|
+
type === "resize" || type === ELEMENT_SIZE_CHANGE || type === "focusin"
|
|
40023
|
+
);
|
|
40024
|
+
};
|
|
40025
|
+
|
|
40026
|
+
const scrollportHeightMap = new WeakMap();
|
|
40027
|
+
/**
|
|
40028
|
+
* Scrolls whatever holds focus inside `popupEl` back into view, if the popup
|
|
40029
|
+
* getting shorter has pushed it out.
|
|
40030
|
+
*
|
|
40031
|
+
* The case this exists for: a field low in the scrolling body of a popup that
|
|
40032
|
+
* also has a footer (box.jsx — with a body, the body is the only thing that
|
|
40033
|
+
* scrolls and the footer is a sibling sitting right under it). Focusing the
|
|
40034
|
+
* field makes the browser scroll it into view, which it does against the
|
|
40035
|
+
* popup's height AT THAT MOMENT; the on-screen keyboard then opens and takes
|
|
40036
|
+
* that height away. The body shrinks, its scrollTop does not move, so the
|
|
40037
|
+
* content slides down relative to the shorter scrollport and the field ends up
|
|
40038
|
+
* past its bottom edge — visually, swallowed by the footer. The browser does
|
|
40039
|
+
* not redo a scroll-into-view it already answered, so this does.
|
|
40040
|
+
*
|
|
40041
|
+
* Only what the shrink itself hid, though: with the keyboard up the user reads
|
|
40042
|
+
* the rest of the popup by scrolling the field away — to reach the submit under
|
|
40043
|
+
* it, typically — and the room keeps changing while they do (a keyboard settling
|
|
40044
|
+
* in two steps, a suggestion strip, a browser bar). Answering each of those by
|
|
40045
|
+
* scrolling the field back takes the popup away from wherever they had just
|
|
40046
|
+
* scrolled it, over and over: what they were reading cannot be reached at all
|
|
40047
|
+
* without blurring the field first, and the popup reads as unscrollable. So the
|
|
40048
|
+
* field is brought back only when it was in view before the room shrank, which
|
|
40049
|
+
* one remembered number answers: a resize moves neither scrollTop nor the
|
|
40050
|
+
* element's offset inside the scrolled content, so measuring against the height
|
|
40051
|
+
* the scrollport HAD is measuring the state before the change.
|
|
40052
|
+
*
|
|
40053
|
+
* Scoped to the field's own scroll container (never the page): a popup traps
|
|
40054
|
+
* scrolling precisely so the document underneath cannot move, and a plain
|
|
40055
|
+
* scrollIntoView walks past a container whose scrollbar isn't visible — see
|
|
40056
|
+
* scrollIntoViewScoped's own doc.
|
|
40057
|
+
*
|
|
40058
|
+
* "nearest": the smallest scroll that makes it visible, and none at all when it
|
|
40059
|
+
* already is. Where it lands is the container's own business — a navi scroller
|
|
40060
|
+
* keeps a band free at its edges so a field never comes back glued to one (see
|
|
40061
|
+
* scroll-padding in box.jsx).
|
|
40062
|
+
*/
|
|
40063
|
+
const keepFocusedElementVisible = (popupEl) => {
|
|
40064
|
+
const { activeElement } = document;
|
|
40065
|
+
if (!activeElement || activeElement === popupEl) {
|
|
40066
|
+
return;
|
|
40067
|
+
}
|
|
40068
|
+
if (!popupEl.contains(activeElement)) {
|
|
40069
|
+
return;
|
|
40070
|
+
}
|
|
40071
|
+
const scrollContainer = getScrollContainer(activeElement);
|
|
40072
|
+
if (!scrollContainer || !popupEl.contains(scrollContainer)) {
|
|
40073
|
+
// What scrolls the field is outside the popup, which means the page: a
|
|
40074
|
+
// popup holds it still on purpose (trapScrollInside), so there is nothing
|
|
40075
|
+
// here to scroll back.
|
|
40076
|
+
return;
|
|
40077
|
+
}
|
|
40078
|
+
const scrollportHeight = scrollContainer.clientHeight;
|
|
40079
|
+
const scrollportHeightBefore = scrollportHeightMap.get(scrollContainer);
|
|
40080
|
+
scrollportHeightMap.set(scrollContainer, scrollportHeight);
|
|
40081
|
+
if (scrollportHeightBefore !== undefined) {
|
|
40082
|
+
if (scrollportHeight >= scrollportHeightBefore) {
|
|
40083
|
+
// Nothing was taken away, so nothing was hidden by this.
|
|
40084
|
+
return;
|
|
40085
|
+
}
|
|
40086
|
+
if (
|
|
40087
|
+
!isVisibleInScrollport(
|
|
40088
|
+
activeElement,
|
|
40089
|
+
scrollContainer,
|
|
40090
|
+
scrollportHeightBefore,
|
|
40091
|
+
)
|
|
40092
|
+
) {
|
|
40093
|
+
return;
|
|
40094
|
+
}
|
|
40095
|
+
}
|
|
40096
|
+
scrollIntoViewScoped(activeElement, {
|
|
40097
|
+
container: scrollContainer,
|
|
40098
|
+
block: "nearest",
|
|
40099
|
+
});
|
|
40100
|
+
};
|
|
40101
|
+
|
|
40102
|
+
// Whether any part of `el` was showing in `container` back when its scrollport
|
|
40103
|
+
// was `scrollportHeight` tall. Both boxes are read now: the container may have
|
|
40104
|
+
// moved as well as shrunk (a centered dialog re-centers itself), and the
|
|
40105
|
+
// difference between the two tops is what that move leaves alone.
|
|
40106
|
+
const isVisibleInScrollport = (el, container, scrollportHeight) => {
|
|
40107
|
+
const elRect = el.getBoundingClientRect();
|
|
40108
|
+
const containerRect = container.getBoundingClientRect();
|
|
40109
|
+
const elTop = elRect.top - containerRect.top + container.scrollTop;
|
|
40110
|
+
const elBottom = elTop + elRect.height;
|
|
40111
|
+
const scrollportTop = container.scrollTop;
|
|
40112
|
+
const scrollportBottom = scrollportTop + scrollportHeight;
|
|
40113
|
+
return elBottom > scrollportTop && elTop < scrollportBottom;
|
|
40114
|
+
};
|
|
40115
|
+
|
|
40116
|
+
/**
|
|
40117
|
+
* Calls `onSettled` once `el`'s current CSS transition is over — via
|
|
40118
|
+
* `transitionend`, with a safety `setTimeout` fallback matching the longest
|
|
40119
|
+
* `transition-duration`, in case nothing actually transitions or an event is
|
|
40120
|
+
* missed.
|
|
40121
|
+
*
|
|
40122
|
+
* Returns a "cancel" function, so a caller whose instance has been superseded
|
|
40123
|
+
* (a fresh open/close about to set its own state) can keep this stale one from
|
|
40124
|
+
* firing later. Cancelling only stops `onSettled`: undoing whatever the caller
|
|
40125
|
+
* did up front is that fresh call's business, not this one's.
|
|
40126
|
+
*/
|
|
40127
|
+
const whenTransitionSettles = (el, onSettled) => {
|
|
40128
|
+
let settled = false;
|
|
40129
|
+
const onTransitionEnd = (transitionEvent) => {
|
|
40130
|
+
if (transitionEvent.target === el) {
|
|
40131
|
+
finish();
|
|
40132
|
+
}
|
|
40133
|
+
};
|
|
40134
|
+
const stopWatching = () => {
|
|
40135
|
+
settled = true;
|
|
40136
|
+
el.removeEventListener("transitionend", onTransitionEnd);
|
|
40137
|
+
clearTimeout(safetyTimeoutId);
|
|
40138
|
+
};
|
|
40139
|
+
const finish = () => {
|
|
40140
|
+
if (settled) {
|
|
40141
|
+
return;
|
|
40142
|
+
}
|
|
40143
|
+
stopWatching();
|
|
40144
|
+
onSettled();
|
|
40145
|
+
};
|
|
40146
|
+
el.addEventListener("transitionend", onTransitionEnd);
|
|
40147
|
+
const durationsInSeconds = getComputedStyle(el)
|
|
40148
|
+
.transitionDuration.split(",")
|
|
40149
|
+
.map((value) => parseFloat(value) || 0);
|
|
40150
|
+
const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
|
|
40151
|
+
const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
|
|
40152
|
+
return () => {
|
|
40153
|
+
if (settled) {
|
|
40154
|
+
return;
|
|
40155
|
+
}
|
|
40156
|
+
stopWatching();
|
|
40157
|
+
};
|
|
40158
|
+
};
|
|
40159
|
+
|
|
40160
|
+
/**
|
|
40161
|
+
* Drops the document's text selection when it lives inside `el`, leaving a
|
|
40162
|
+
* selection made elsewhere on the page alone.
|
|
40163
|
+
*
|
|
40164
|
+
* A popup being closed takes its content with it, and a selection is a claim
|
|
40165
|
+
* on content the user can still act on: kept, it would outlive the surface it
|
|
40166
|
+
* was made on — painted on the box for the length of its exit transition, with
|
|
40167
|
+
* the handles and the copy toolbar a phone draws from the live selection
|
|
40168
|
+
* hanging over something that is going away — and reappear with the box on
|
|
40169
|
+
* the next opening. Dropping the selection is the only way to remove that
|
|
40170
|
+
* chrome: a `user-select: none` on the closing box hides the highlight in some
|
|
40171
|
+
* browsers only, and the handles are the browser's own, drawn from the
|
|
40172
|
+
* selection object rather than from any style.
|
|
40173
|
+
*/
|
|
40174
|
+
const clearTextSelectionInside = (el) => {
|
|
40175
|
+
const selection = window.getSelection();
|
|
40176
|
+
if (!selection || selection.rangeCount === 0) {
|
|
40177
|
+
return;
|
|
40178
|
+
}
|
|
40179
|
+
const range = selection.getRangeAt(0);
|
|
40180
|
+
if (!el.contains(range.commonAncestorContainer)) {
|
|
40181
|
+
return;
|
|
40182
|
+
}
|
|
40183
|
+
selection.removeAllRanges();
|
|
40184
|
+
};
|
|
40185
|
+
|
|
40186
|
+
/**
|
|
40187
|
+
* Disables pointer-events on `el` until its current CSS transition settles —
|
|
40188
|
+
* avoids the cursor changing/something becoming clickable while the popup is
|
|
40189
|
+
* still visually moving into or out of place.
|
|
40190
|
+
*
|
|
40191
|
+
* Returns whenTransitionSettles' own "cancel" function: it doesn't restore
|
|
40192
|
+
* pointer-events, since a fresh call for the next open/close is about to set
|
|
40193
|
+
* its own state.
|
|
40194
|
+
*/
|
|
40195
|
+
const suppressPointerEventsDuringTransition = (el) => {
|
|
40196
|
+
el.style.pointerEvents = "none";
|
|
40197
|
+
return whenTransitionSettles(el, () => {
|
|
40198
|
+
el.style.pointerEvents = "";
|
|
40199
|
+
});
|
|
40200
|
+
};
|
|
40201
|
+
|
|
40202
|
+
/**
|
|
40203
|
+
* Hides the backdrop, deferring until the browser's matching "click" fires
|
|
40204
|
+
* when `closeEvent` was triggered by a mousedown (see popover.jsx's top
|
|
40205
|
+
* comment for why) — same capture-phase-on-document pattern as
|
|
40206
|
+
* armSuppressNextOpenRequest in open_controller.js, which a plain timeout
|
|
40207
|
+
* can't safely replace: mouseup (and the click that follows it) can land an
|
|
40208
|
+
* arbitrarily long time after mousedown (the user is still holding the
|
|
40209
|
+
* button down), so a short timeout can fire first and hide the backdrop
|
|
40210
|
+
* before its own click ever arrives. A capture-phase listener on document
|
|
40211
|
+
* fires for every click regardless of what any bubble-phase handler does
|
|
40212
|
+
* downstream, so no fallback timer is needed.
|
|
40213
|
+
*
|
|
40214
|
+
* `hide` is the caller's own way to actually hide the backdrop
|
|
40215
|
+
* (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
|
|
40216
|
+
* for a plain div) — this helper only owns the mousedown/click timing.
|
|
40217
|
+
*
|
|
40218
|
+
* Returns a disarm function (or undefined if hidden immediately), so a
|
|
40219
|
+
* fresh open can cancel a pending hide it's about to make redundant.
|
|
40220
|
+
*/
|
|
40221
|
+
const armPointerDownOutsideClose = (closeEvent, hide) => {
|
|
40222
|
+
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
40223
|
+
if (!mousedownEvent) {
|
|
40224
|
+
hide();
|
|
40225
|
+
return undefined;
|
|
40226
|
+
}
|
|
40227
|
+
const onClick = () => {
|
|
40228
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
40229
|
+
hide();
|
|
40230
|
+
};
|
|
40231
|
+
document.addEventListener("click", onClick, { capture: true });
|
|
40232
|
+
return () => {
|
|
40233
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
40234
|
+
};
|
|
40235
|
+
};
|
|
40236
|
+
|
|
40237
|
+
/**
|
|
40238
|
+
* Maps a positionArea y/x pair to a concrete `navi-animation` value (a
|
|
40239
|
+
* `prefix` plus a direction word), or `null` if both axes overlap the anchor
|
|
40240
|
+
* (no direction at all — that's `resolvedAnimationKind === "scaling"`
|
|
40241
|
+
* territory instead, see resolveAutoAnimationKind below).
|
|
40242
|
+
*
|
|
40243
|
+
* `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
|
|
40244
|
+
* when docked) keeps the word as the compass direction the popup comes
|
|
40245
|
+
* from: placed "top" (a point/corner), it slides in from the top.
|
|
40246
|
+
* `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
|
|
40247
|
+
* direction instead, the opposite compass point: placed "top" of the
|
|
40248
|
+
* anchor, it moves/grows up, away from the anchor (which sits below it).
|
|
40249
|
+
*
|
|
40250
|
+
* "inset-*"/"center" contribute no direction on their axis either way.
|
|
40251
|
+
*/
|
|
40252
|
+
const resolveDirectionValue = (y, x, { prefix }) => {
|
|
40253
|
+
const yWord =
|
|
40254
|
+
y === "top"
|
|
40255
|
+
? prefix === "expand"
|
|
40256
|
+
? "up"
|
|
40257
|
+
: "top"
|
|
40258
|
+
: y === "bottom"
|
|
40259
|
+
? prefix === "expand"
|
|
40260
|
+
? "down"
|
|
40261
|
+
: "bottom"
|
|
40262
|
+
: null;
|
|
40263
|
+
const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
|
|
40264
|
+
if (!yWord && !xWord) {
|
|
40265
|
+
return null;
|
|
40266
|
+
}
|
|
40267
|
+
return yWord && xWord
|
|
40268
|
+
? `${prefix}-${yWord}-${xWord}`
|
|
40269
|
+
: `${prefix}-${yWord || xWord}`;
|
|
40270
|
+
};
|
|
40271
|
+
|
|
40272
|
+
/**
|
|
40273
|
+
* Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
|
|
40274
|
+
* — picked for any real anchor, or for a point/corner placed dead-center
|
|
40275
|
+
* (both positionArea axes overlapping — there's no sensible direction to
|
|
40276
|
+
* slide from in that case). "sliding" otherwise. `anchor` is `undefined`
|
|
40277
|
+
* for any no-anchor/docked case (Dialog always, Popover's own custom
|
|
40278
|
+
* renderer when there's no real anchor), so this collapses to "scaling"
|
|
40279
|
+
* there only for the dead-center case, "sliding" otherwise. The two
|
|
40280
|
+
* "overlapping" booleans below describe the *positionArea* itself (a bare
|
|
40281
|
+
* word vs. "inset-"/"center"), not anything about the anchor — they'd
|
|
40282
|
+
* mean exactly the same thing even with no anchor at all, since it's the
|
|
40283
|
+
* position strategy, not the anchor, that decides whether there's a
|
|
40284
|
+
* direction to slide from.
|
|
40285
|
+
*/
|
|
40286
|
+
const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
|
|
40287
|
+
const yIsOverlapping =
|
|
40288
|
+
parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
|
|
40289
|
+
const xIsOverlapping =
|
|
40290
|
+
parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
|
|
40291
|
+
return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
|
|
40292
|
+
};
|
|
40293
|
+
|
|
40294
|
+
/**
|
|
40295
|
+
* A popup reads "outside" from its own border box: what a press lands on
|
|
40296
|
+
* decides nothing on its own (a genuine backdrop press and a press on the
|
|
40297
|
+
* popup's padding both report the popup element as their target, there being
|
|
40298
|
+
* no real ::backdrop node to be one), so the rectangle is what tells them
|
|
40299
|
+
* apart. That holds as long as the box and what the popup paints are the same
|
|
40300
|
+
* thing — which stops being true for a popup with no surface of its own
|
|
40301
|
+
* (`backgroundColor="transparent"`, no shadow, no padding): what the eye reads
|
|
40302
|
+
* as backdrop is then inside the box, and a press there stays a press on the
|
|
40303
|
+
* popup.
|
|
40304
|
+
*
|
|
40305
|
+
* `data-navi-popup-outside` is how a caller says which of its own boxes are
|
|
40306
|
+
* not the surface. It is opt-in because navi cannot infer it — a background
|
|
40307
|
+
* can come from anywhere — while the caller who made the popup see-through
|
|
40308
|
+
* knows exactly which box is decoration and which is paper.
|
|
40309
|
+
*
|
|
40310
|
+
* The marker answers for the element it is on, never for its descendants: a
|
|
40311
|
+
* box painted inside a marked one is still surface, so a row can be marked and
|
|
40312
|
+
* its empty halves read as backdrop while presses on the controls it holds do
|
|
40313
|
+
* not dismiss anything. A descendant with `pointer-events: none` never becomes
|
|
40314
|
+
* a press target at all, so the marked box answers in its place — while the
|
|
40315
|
+
* same declaration on the marked box itself takes it out of hit-testing and
|
|
40316
|
+
* makes the marker unreachable (see warnAboutUnreachableOutsideRegions).
|
|
40317
|
+
*/
|
|
40318
|
+
const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
|
|
40319
|
+
|
|
40320
|
+
/**
|
|
40321
|
+
* `data-navi-popup-inside` is the other direction: a box of the PAGE the
|
|
40322
|
+
* caller says belongs to a popup, so a press on it is not outside that popup
|
|
40323
|
+
* even though it lies beyond the border box — a card whose press is what
|
|
40324
|
+
* fills the one panel of a board. Its value names the popup by `id`, the way
|
|
40325
|
+
* `commandfor` does (several ids, space separated), because the exemption is
|
|
40326
|
+
* one popup's: a menu open beside the board still closes on the same press.
|
|
40327
|
+
*
|
|
40328
|
+
* Unlike the outside marker it answers for its whole subtree: the outside
|
|
40329
|
+
* marker carves free space out of a surface whose controls stay surface, this
|
|
40330
|
+
* one adds a whole thing — the card, its count, its buttons — to the popup's
|
|
40331
|
+
* own ground, and nothing in it is a dismissal.
|
|
40332
|
+
*/
|
|
40333
|
+
const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
|
|
40334
|
+
// Whether `target` is in a box of the page declared as part of the popup
|
|
40335
|
+
// named `popupId`.
|
|
40336
|
+
const isDeclaredInsidePopup = (target, popupId) =>
|
|
40337
|
+
Boolean(popupId) &&
|
|
40338
|
+
Boolean(target?.closest?.(`[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupId)}"]`));
|
|
40339
|
+
|
|
40340
|
+
/**
|
|
40341
|
+
* What a press landing on a region the caller declared as not-its-surface
|
|
40342
|
+
* does: exactly what the same press on the backdrop would do.
|
|
40343
|
+
*
|
|
40344
|
+
* Lives on the popup's own content element rather than in the backdrop's
|
|
40345
|
+
* handler, for both renderers: the backdrop is a sibling behind the popup
|
|
40346
|
+
* (never an ancestor), so a press inside the popup's box never reaches it.
|
|
40347
|
+
*/
|
|
40348
|
+
const handlePressOnOutsideRegion = (
|
|
40349
|
+
mouseDownEvent,
|
|
40350
|
+
{ popupEl, openController, pressOutside },
|
|
40351
|
+
) => {
|
|
40352
|
+
if (mouseDownEvent.button !== 0) {
|
|
40353
|
+
return;
|
|
40354
|
+
}
|
|
40355
|
+
const { target } = mouseDownEvent;
|
|
40356
|
+
if (!target.hasAttribute(OUTSIDE_REGION_ATTRIBUTE)) {
|
|
40357
|
+
return;
|
|
40358
|
+
}
|
|
40359
|
+
if (openedDuringThisPress(openController)) {
|
|
40360
|
+
// The release of the press that opened it is not somebody dismissing it
|
|
40361
|
+
// (see openedDuringThisPress).
|
|
40362
|
+
return;
|
|
40363
|
+
}
|
|
40364
|
+
// A popup opens inside its opener's own subtree, so a press on a region
|
|
40365
|
+
// belonging to a popup nested in this one bubbles through here too — and it
|
|
40366
|
+
// is a press on what is in front, not on this popup's own decoration.
|
|
40367
|
+
if (
|
|
40368
|
+
target.closest(`[navi-control="dialog"], [navi-control="popover"]`) !==
|
|
40369
|
+
popupEl
|
|
40370
|
+
) {
|
|
40371
|
+
return;
|
|
40372
|
+
}
|
|
40373
|
+
if (pressOutside === "capture") {
|
|
40374
|
+
mouseDownEvent.preventDefault();
|
|
40375
|
+
return;
|
|
40376
|
+
}
|
|
40377
|
+
if (pressOutside === "close" || pressOutside === "cancel") {
|
|
40378
|
+
openController.requestClose(mouseDownEvent, {
|
|
40379
|
+
isCancel: pressOutside === "cancel",
|
|
40380
|
+
});
|
|
40381
|
+
}
|
|
40382
|
+
};
|
|
40383
|
+
|
|
40384
|
+
/**
|
|
40385
|
+
* Hears an outside press from the document itself, for a popup with no
|
|
40386
|
+
* backdrop of its own to catch one: a modal `<dialog>` (whose native
|
|
40387
|
+
* `::backdrop` dispatches nothing a handler can be put on) and any popup the
|
|
40388
|
+
* caller asked to leave the page reachable (`backdrop={false}`).
|
|
40389
|
+
*
|
|
40390
|
+
* The press is read and never taken: no `preventDefault`, no
|
|
40391
|
+
* `stopPropagation`, and the listener is on `document` rather than on
|
|
40392
|
+
* anything covering the page — so what the press landed on hears it too, in
|
|
40393
|
+
* the same gesture, and the popup only takes note. That is the whole point of
|
|
40394
|
+
* a popup with no wall: a press outside is a press on the page, and the page
|
|
40395
|
+
* answers it. The one popup where that cannot hold is the modal dialog, where
|
|
40396
|
+
* the browser has already made everything behind inert before this runs.
|
|
40397
|
+
*
|
|
40398
|
+
* `pressEventType` says which event IS the press, and the two callers want
|
|
40399
|
+
* opposite ones:
|
|
40400
|
+
*
|
|
40401
|
+
* - `"pointerdown"` for a popup with no wall: a page with no wall over it is
|
|
40402
|
+
* a page whose own elements arbitrate their presses, and cancelling a
|
|
40403
|
+
* `pointerdown` — what a drag source and a control keeping the focus where
|
|
40404
|
+
* it is both do — suppresses every compatibility mouse event that would have
|
|
40405
|
+
* followed it. The press happens, reaches its target, and no `mousedown` is
|
|
40406
|
+
* ever dispatched for it. Reading the pointer event is reading the press
|
|
40407
|
+
* itself, whatever anything does with it afterwards, and it is the same
|
|
40408
|
+
* event the drag and surface detectors read.
|
|
40409
|
+
* - `"mousedown"` for the modal dialog, whose native `::backdrop` exists to
|
|
40410
|
+
* spend the press. The page's controls act on `mousedown`, and on a touch
|
|
40411
|
+
* screen that is not the same moment as `pointerdown`: the compatibility
|
|
40412
|
+
* mouse events are synthesized at `touchend` and hit-tested again then.
|
|
40413
|
+
* Closing on `pointerdown` takes the `::backdrop` away before that second
|
|
40414
|
+
* hit-test, so the `mousedown` lands on whatever the page holds under the
|
|
40415
|
+
* finger, and one tap both dismisses the dialog and presses the page. Read
|
|
40416
|
+
* on `mousedown`, the wall is still up when the press the page acts on is
|
|
40417
|
+
* aimed, and it catches it. Nothing behind a modal wall can cancel that
|
|
40418
|
+
* press, since nothing behind it hears the `pointerdown` at all.
|
|
40419
|
+
*
|
|
40420
|
+
* Capture phase, so a handler downstream that stops propagation cannot keep
|
|
40421
|
+
* the popup open either — the press is still outside whatever is made of it.
|
|
40422
|
+
*
|
|
40423
|
+
* Returns a cleanup that removes the listener.
|
|
40424
|
+
*/
|
|
40425
|
+
const armOutsidePressClose = (
|
|
40426
|
+
popupEl,
|
|
40427
|
+
{ openController, pressOutside, pressEventType = "pointerdown" },
|
|
40428
|
+
) => {
|
|
40429
|
+
const onDocumentPointerDown = (pointerDownEvent) => {
|
|
40430
|
+
if (pointerDownEvent.button !== 0) {
|
|
40431
|
+
return;
|
|
40432
|
+
}
|
|
40433
|
+
if (openedDuringThisPress(openController)) {
|
|
40434
|
+
// The release of the press that opened it is not somebody dismissing it
|
|
40435
|
+
// (see openedDuringThisPress).
|
|
40436
|
+
return;
|
|
40437
|
+
}
|
|
40438
|
+
// The press landed inside another popup: that is a press on what is in
|
|
40439
|
+
// front, not outside. Asking the target where it lives rather than asking
|
|
40440
|
+
// this popup whether it was pushed — a popup in front does not have to be
|
|
40441
|
+
// one this popup knows about. In front means neither inside nor around: a
|
|
40442
|
+
// popup nested in this one falls to the containment check below as the
|
|
40443
|
+
// inside press it is, and one this popup opened INSIDE (a bubble rendered
|
|
40444
|
+
// in a sheet's content) is around it, so what that popup holds is outside
|
|
40445
|
+
// this one exactly like the rest of the page.
|
|
40446
|
+
const popupUnderPointer = pointerDownEvent.target.closest?.(
|
|
40447
|
+
`[navi-control="dialog"], [navi-control="popover"]`,
|
|
40448
|
+
);
|
|
40449
|
+
if (
|
|
40450
|
+
popupUnderPointer &&
|
|
40451
|
+
popupUnderPointer !== popupEl &&
|
|
40452
|
+
!popupEl.contains(popupUnderPointer) &&
|
|
40453
|
+
!popupUnderPointer.contains(popupEl)
|
|
40454
|
+
) {
|
|
40455
|
+
return;
|
|
40456
|
+
}
|
|
40457
|
+
if (isDeclaredInsidePopup(pointerDownEvent.target, popupEl.id)) {
|
|
40458
|
+
return;
|
|
40459
|
+
}
|
|
40460
|
+
// Real DOM containment wins over the rectangle below — an element
|
|
40461
|
+
// genuinely inside the popup (`overflow: visible`, a negative margin, an
|
|
40462
|
+
// absolutely-positioned child) can be painted outside its border box, and
|
|
40463
|
+
// a press there is not outside just because its coordinates are. The
|
|
40464
|
+
// popup element itself is excluded (contains() answers true for it): a
|
|
40465
|
+
// press on a modal dialog's native backdrop reports the dialog as its
|
|
40466
|
+
// target, there being no `::backdrop` node to be one, so treating that as
|
|
40467
|
+
// contained would keep the rectangle from ever running.
|
|
40468
|
+
if (
|
|
40469
|
+
pointerDownEvent.target !== popupEl &&
|
|
40470
|
+
popupEl.contains(pointerDownEvent.target)
|
|
40471
|
+
) {
|
|
40472
|
+
return;
|
|
40473
|
+
}
|
|
40474
|
+
const rect = popupEl.getBoundingClientRect();
|
|
40475
|
+
const isOutside =
|
|
40476
|
+
pointerDownEvent.clientX < rect.left ||
|
|
40477
|
+
pointerDownEvent.clientX > rect.right ||
|
|
40478
|
+
pointerDownEvent.clientY < rect.top ||
|
|
40479
|
+
pointerDownEvent.clientY > rect.bottom;
|
|
40480
|
+
if (!isOutside) {
|
|
40481
|
+
return;
|
|
40482
|
+
}
|
|
40483
|
+
openController.requestClose(pointerDownEvent, {
|
|
40484
|
+
isCancel: pressOutside === "cancel",
|
|
40485
|
+
});
|
|
40486
|
+
};
|
|
40487
|
+
document.addEventListener(pressEventType, onDocumentPointerDown, {
|
|
40488
|
+
capture: true,
|
|
40489
|
+
});
|
|
40490
|
+
return () => {
|
|
40491
|
+
document.removeEventListener(pressEventType, onDocumentPointerDown, {
|
|
40492
|
+
capture: true,
|
|
40493
|
+
});
|
|
40494
|
+
};
|
|
40495
|
+
};
|
|
40496
|
+
|
|
40447
40497
|
/**
|
|
40448
40498
|
* When a popup builds what it holds, and when it throws it away.
|
|
40449
40499
|
*
|
|
@@ -49161,7 +49211,7 @@ const startNetworkMonitoring = () => {
|
|
|
49161
49211
|
};
|
|
49162
49212
|
|
|
49163
49213
|
installImportMetaCssBuild(import.meta);
|
|
49164
|
-
const css$
|
|
49214
|
+
const css$11 = /* css */`.navi_loading_indicator_fluid_container {
|
|
49165
49215
|
border-radius: inherit;
|
|
49166
49216
|
opacity: 1;
|
|
49167
49217
|
width: 100%;
|
|
@@ -49192,7 +49242,7 @@ const LoadingIndicatorFluid = ({
|
|
|
49192
49242
|
visuallyHidden,
|
|
49193
49243
|
...rest
|
|
49194
49244
|
}) => {
|
|
49195
|
-
import.meta.css = [css$
|
|
49245
|
+
import.meta.css = [css$11, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
|
|
49196
49246
|
const ref = useRef(null);
|
|
49197
49247
|
// The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
|
|
49198
49248
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
@@ -49394,7 +49444,7 @@ const LoadingRectangleSvg = ({
|
|
|
49394
49444
|
};
|
|
49395
49445
|
|
|
49396
49446
|
installImportMetaCssBuild(import.meta);
|
|
49397
|
-
const css
|
|
49447
|
+
const css$10 = /* css */`.navi_loading_outline_wrapper {
|
|
49398
49448
|
top: max(var(--loading-outline-min-inset, -100vh),
|
|
49399
49449
|
var(--loading-rectangle-top, 0px));
|
|
49400
49450
|
right: max(var(--loading-outline-min-inset, -100vh),
|
|
@@ -49415,7 +49465,7 @@ const css$$ = /* css */`.navi_loading_outline_wrapper {
|
|
|
49415
49465
|
}
|
|
49416
49466
|
`;
|
|
49417
49467
|
const LoadingOutline = props => {
|
|
49418
|
-
import.meta.css = [css
|
|
49468
|
+
import.meta.css = [css$10, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
|
|
49419
49469
|
if (props.containerRef) {
|
|
49420
49470
|
const container = props.containerRef.current;
|
|
49421
49471
|
if (!container) {
|
|
@@ -49617,7 +49667,7 @@ const flush = () => {
|
|
|
49617
49667
|
};
|
|
49618
49668
|
|
|
49619
49669
|
installImportMetaCssBuild(import.meta);
|
|
49620
|
-
const css
|
|
49670
|
+
const css$$ = /* css */`.navi_text_anchor {
|
|
49621
49671
|
vertical-align: baseline;
|
|
49622
49672
|
user-select: none;
|
|
49623
49673
|
overflow: hidden;
|
|
@@ -49655,7 +49705,7 @@ const TextAnchor = ({
|
|
|
49655
49705
|
textSize,
|
|
49656
49706
|
lineLayout
|
|
49657
49707
|
}) => {
|
|
49658
|
-
import.meta.css = [css
|
|
49708
|
+
import.meta.css = [css$$, "@jsenv/navi/src/text/text_anchor.jsx"];
|
|
49659
49709
|
const anchorRef = useRef();
|
|
49660
49710
|
// The correction reads the layout, with the other reads of the commit (see
|
|
49661
49711
|
// layout_batch.js) — an anchor per icon, each measured between two writes,
|
|
@@ -49906,7 +49956,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
|
|
|
49906
49956
|
};
|
|
49907
49957
|
|
|
49908
49958
|
installImportMetaCssBuild(import.meta);
|
|
49909
|
-
const css$
|
|
49959
|
+
const css$_ = /* css */`@layer navi {
|
|
49910
49960
|
.navi_text {
|
|
49911
49961
|
&[data-skeleton] {
|
|
49912
49962
|
border-radius: .2em;
|
|
@@ -50498,7 +50548,7 @@ const TextShrinkWrap = props => {
|
|
|
50498
50548
|
});
|
|
50499
50549
|
};
|
|
50500
50550
|
const TextUI = props => {
|
|
50501
|
-
import.meta.css = [css$
|
|
50551
|
+
import.meta.css = [css$_, "@jsenv/navi/src/text/text.jsx"];
|
|
50502
50552
|
let {
|
|
50503
50553
|
ref,
|
|
50504
50554
|
spacing,
|
|
@@ -50736,7 +50786,7 @@ const Icon = ({
|
|
|
50736
50786
|
fillLine,
|
|
50737
50787
|
...props
|
|
50738
50788
|
}) => {
|
|
50739
|
-
import.meta.css = [css$
|
|
50789
|
+
import.meta.css = [css$_, "@jsenv/navi/src/text/text.jsx"];
|
|
50740
50790
|
const innerChildren = href ? jsx("svg", {
|
|
50741
50791
|
width: "100%",
|
|
50742
50792
|
height: "100%",
|
|
@@ -51070,7 +51120,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
|
|
|
51070
51120
|
};
|
|
51071
51121
|
|
|
51072
51122
|
installImportMetaCssBuild(import.meta);
|
|
51073
|
-
const css$
|
|
51123
|
+
const css$Z = /* css */`@layer navi {
|
|
51074
51124
|
.navi_link {
|
|
51075
51125
|
--link-border-radius: unset;
|
|
51076
51126
|
--link-outline-color: var(--navi-focus-outline-color);
|
|
@@ -51544,7 +51594,7 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
|
|
|
51544
51594
|
* @param {boolean} [props.readOnly]
|
|
51545
51595
|
*/
|
|
51546
51596
|
const Link = props => {
|
|
51547
|
-
import.meta.css = [css$
|
|
51597
|
+
import.meta.css = [css$Z, "@jsenv/navi/src/nav/link/link.jsx"];
|
|
51548
51598
|
if (props.route) {
|
|
51549
51599
|
return jsx(LinkWithRoute, {
|
|
51550
51600
|
...props
|
|
@@ -52104,7 +52154,7 @@ const rememberProgressAnimation = (navElement, travel) => {
|
|
|
52104
52154
|
travel.progressAnimation = animation;
|
|
52105
52155
|
travel.progressFrom = animation ? parseFloat(animation.effect.getKeyframes()[0]["--slide-travel-progress"]) || 0 : 0;
|
|
52106
52156
|
};
|
|
52107
|
-
const css$
|
|
52157
|
+
const css$Y = /* css */`@layer navi {
|
|
52108
52158
|
.navi_nav {
|
|
52109
52159
|
--nav-border: none;
|
|
52110
52160
|
--nav-padding: 0px;
|
|
@@ -52373,7 +52423,7 @@ const Nav = ({
|
|
|
52373
52423
|
slideContainer,
|
|
52374
52424
|
...props
|
|
52375
52425
|
}) => {
|
|
52376
|
-
import.meta.css = [css$
|
|
52426
|
+
import.meta.css = [css$Y, "@jsenv/navi/src/nav/link/nav.jsx"];
|
|
52377
52427
|
const defaultRef = useRef();
|
|
52378
52428
|
props.ref = props.ref || defaultRef;
|
|
52379
52429
|
const navRef = props.ref;
|
|
@@ -53249,7 +53299,7 @@ const buildBinderCenterlinePathBelow = ({
|
|
|
53249
53299
|
};
|
|
53250
53300
|
|
|
53251
53301
|
installImportMetaCssBuild(import.meta);
|
|
53252
|
-
const css$
|
|
53302
|
+
const css$X = /* css */`@layer navi {
|
|
53253
53303
|
.navi_binder {
|
|
53254
53304
|
--binder-border-width: var(--navi-control-border-width);
|
|
53255
53305
|
--binder-border-radius: var(--navi-control-border-radius);
|
|
@@ -53536,7 +53586,7 @@ const Binder = ({
|
|
|
53536
53586
|
pagePadding,
|
|
53537
53587
|
...props
|
|
53538
53588
|
}) => {
|
|
53539
|
-
import.meta.css = [css$
|
|
53589
|
+
import.meta.css = [css$X, "@jsenv/navi/src/nav/binder/binder.jsx"];
|
|
53540
53590
|
const items = toChildArray(children).map((child, index) => {
|
|
53541
53591
|
const {
|
|
53542
53592
|
value: itemValue,
|
|
@@ -53986,7 +54036,7 @@ const writeSpace = (area) => {
|
|
|
53986
54036
|
};
|
|
53987
54037
|
|
|
53988
54038
|
installImportMetaCssBuild(import.meta);
|
|
53989
|
-
const css$
|
|
54039
|
+
const css$W = /* css */`@layer navi {
|
|
53990
54040
|
:root {
|
|
53991
54041
|
--navi-fixed-bar-width: 56px;
|
|
53992
54042
|
--navi-fixed-bar-height: 56px;
|
|
@@ -54109,7 +54159,7 @@ const FixedBar = ({
|
|
|
54109
54159
|
border = true,
|
|
54110
54160
|
...props
|
|
54111
54161
|
}) => {
|
|
54112
|
-
import.meta.css = [css$
|
|
54162
|
+
import.meta.css = [css$W, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
|
|
54113
54163
|
const defaultRef = useRef();
|
|
54114
54164
|
props.ref = props.ref || defaultRef;
|
|
54115
54165
|
// Said with the width the border rule reads rather than with an attribute of
|
|
@@ -54203,7 +54253,7 @@ const FixedBar = ({
|
|
|
54203
54253
|
// Subpixel layout rounds rectangles up on boxes that fit exactly.
|
|
54204
54254
|
const OVERFLOW_TOLERANCE = 1;
|
|
54205
54255
|
|
|
54206
|
-
const css$
|
|
54256
|
+
const css$V = /* css */ `
|
|
54207
54257
|
[data-navi-overflow-x] {
|
|
54208
54258
|
outline: 2px dashed #e74c3c;
|
|
54209
54259
|
outline-offset: -2px;
|
|
@@ -54227,7 +54277,7 @@ const detectHorizontalOverflow = ({
|
|
|
54227
54277
|
let styleEl = null;
|
|
54228
54278
|
if (highlight) {
|
|
54229
54279
|
styleEl = document.createElement("style");
|
|
54230
|
-
styleEl.textContent = css$
|
|
54280
|
+
styleEl.textContent = css$V;
|
|
54231
54281
|
document.head.appendChild(styleEl);
|
|
54232
54282
|
}
|
|
54233
54283
|
|
|
@@ -54317,7 +54367,11 @@ const findOverflows = (root) => {
|
|
|
54317
54367
|
if (style.display === "none" || style.position === "fixed") {
|
|
54318
54368
|
continue;
|
|
54319
54369
|
}
|
|
54320
|
-
if (
|
|
54370
|
+
if (
|
|
54371
|
+
el.hasAttribute("popover") ||
|
|
54372
|
+
// An idle PopupGroup is a box of the page (see popup_group.jsx).
|
|
54373
|
+
(el.tagName === "DIALOG" && !el.hasAttribute("navi-popup-group"))
|
|
54374
|
+
) {
|
|
54321
54375
|
continue;
|
|
54322
54376
|
}
|
|
54323
54377
|
const rect = el.getBoundingClientRect();
|
|
@@ -54383,7 +54437,7 @@ const useFocusGroup = (
|
|
|
54383
54437
|
|
|
54384
54438
|
installImportMetaCssBuild(import.meta);
|
|
54385
54439
|
const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
|
|
54386
|
-
const css$
|
|
54440
|
+
const css$U = /* css */`.navi_summary_marker {
|
|
54387
54441
|
flex-shrink: 0;
|
|
54388
54442
|
width: 1em;
|
|
54389
54443
|
height: 1em;
|
|
@@ -54475,7 +54529,7 @@ const SummaryMarker = ({
|
|
|
54475
54529
|
loading,
|
|
54476
54530
|
openDirection = "down"
|
|
54477
54531
|
}) => {
|
|
54478
|
-
import.meta.css = [css$
|
|
54532
|
+
import.meta.css = [css$U, "@jsenv/navi/src/control/details/summary_marker.jsx"];
|
|
54479
54533
|
const showLoading = useDebounceTrue(loading, 300);
|
|
54480
54534
|
return jsx("span", {
|
|
54481
54535
|
className: "navi_summary_marker",
|
|
@@ -54521,7 +54575,7 @@ const SummaryMarker = ({
|
|
|
54521
54575
|
};
|
|
54522
54576
|
|
|
54523
54577
|
installImportMetaCssBuild(import.meta);
|
|
54524
|
-
const css$
|
|
54578
|
+
const css$T = /* css */`.navi_details {
|
|
54525
54579
|
z-index: 1;
|
|
54526
54580
|
flex-direction: column;
|
|
54527
54581
|
flex-shrink: 0;
|
|
@@ -54566,7 +54620,7 @@ const Details = props => {
|
|
|
54566
54620
|
return details;
|
|
54567
54621
|
};
|
|
54568
54622
|
const DetailsField = props => {
|
|
54569
|
-
import.meta.css = [css$
|
|
54623
|
+
import.meta.css = [css$T, "@jsenv/navi/src/control/details/details.jsx"];
|
|
54570
54624
|
const {
|
|
54571
54625
|
ref,
|
|
54572
54626
|
persists,
|
|
@@ -54732,7 +54786,7 @@ const DetailsFieldContent = ({
|
|
|
54732
54786
|
};
|
|
54733
54787
|
|
|
54734
54788
|
installImportMetaCssBuild(import.meta);
|
|
54735
|
-
const css$
|
|
54789
|
+
const css$S = /* css */`.navi_expandable {
|
|
54736
54790
|
flex-direction: column;
|
|
54737
54791
|
flex-shrink: 0;
|
|
54738
54792
|
display: flex;
|
|
@@ -54976,7 +55030,7 @@ const useExpandableContext = partName => {
|
|
|
54976
55030
|
* around them (docs/control_object.md, "A control that is not there").
|
|
54977
55031
|
*/
|
|
54978
55032
|
const Expandable = props => {
|
|
54979
|
-
import.meta.css = [css$
|
|
55033
|
+
import.meta.css = [css$S, "@jsenv/navi/src/control/expandable/expandable.jsx"];
|
|
54980
55034
|
/* The open props are read from `props` by the open controller below; they
|
|
54981
55035
|
are named here only to keep them out of `rest`, and so out of the DOM. */
|
|
54982
55036
|
/* eslint-disable no-unused-vars */
|
|
@@ -55957,7 +56011,7 @@ const useAccentColorAttributes = (
|
|
|
55957
56011
|
};
|
|
55958
56012
|
|
|
55959
56013
|
installImportMetaCssBuild(import.meta);
|
|
55960
|
-
const css$
|
|
56014
|
+
const css$R = /* css */`@layer navi {
|
|
55961
56015
|
.navi_button {
|
|
55962
56016
|
--button-border-radius: var(--navi-control-border-radius);
|
|
55963
56017
|
--button-border-width: var(--navi-control-border-width);
|
|
@@ -56261,7 +56315,7 @@ a.navi_button {
|
|
|
56261
56315
|
}
|
|
56262
56316
|
`;
|
|
56263
56317
|
const ButtonUI = props => {
|
|
56264
|
-
import.meta.css = [css$
|
|
56318
|
+
import.meta.css = [css$R, "@jsenv/navi/src/control/input/button_ui.jsx"];
|
|
56265
56319
|
const {
|
|
56266
56320
|
ref,
|
|
56267
56321
|
// href/link
|
|
@@ -56476,7 +56530,7 @@ const ButtonShadow = () => {
|
|
|
56476
56530
|
markAsOutsideTextFlow(ButtonShadow);
|
|
56477
56531
|
|
|
56478
56532
|
installImportMetaCssBuild(import.meta);
|
|
56479
|
-
const css$
|
|
56533
|
+
const css$Q = /* css */`.navi_button_confirm {
|
|
56480
56534
|
align-items: center;
|
|
56481
56535
|
gap: var(--navi-s);
|
|
56482
56536
|
flex-wrap: wrap;
|
|
@@ -56504,7 +56558,7 @@ const ButtonConfirm = ({
|
|
|
56504
56558
|
cancelTestId,
|
|
56505
56559
|
...props
|
|
56506
56560
|
}) => {
|
|
56507
|
-
import.meta.css = [css$
|
|
56561
|
+
import.meta.css = [css$Q, "@jsenv/navi/src/control/input/button_confirm.jsx"];
|
|
56508
56562
|
const [asking, setAsking] = useState(false);
|
|
56509
56563
|
const askingRef = useRef(null);
|
|
56510
56564
|
const cancelRef = useRef(null);
|
|
@@ -56965,7 +57019,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
56965
57019
|
const Button = /*#__PURE__*/createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonConfirmResolver, ButtonHrefResolver, ButtonUI]);
|
|
56966
57020
|
|
|
56967
57021
|
installImportMetaCssBuild(import.meta);
|
|
56968
|
-
const css$
|
|
57022
|
+
const css$P = /* css */`.navi_control_swap {
|
|
56969
57023
|
font-size: var(--navi-control-font-size);
|
|
56970
57024
|
--x-control-swap-size: var(--navi-control-swap-size, calc(var(--navi-control-line-height) + 2 * var(--navi-s) + 2 *
|
|
56971
57025
|
var(--navi-control-border-width)));
|
|
@@ -57070,7 +57124,7 @@ const css$O = /* css */`.navi_control_swap {
|
|
|
57070
57124
|
* is the space between the boxes.
|
|
57071
57125
|
*/
|
|
57072
57126
|
const ControlSwap = props => {
|
|
57073
|
-
import.meta.css = [css$
|
|
57127
|
+
import.meta.css = [css$P, "@jsenv/navi/src/control/control_swap/control_swap.jsx"];
|
|
57074
57128
|
const {
|
|
57075
57129
|
value,
|
|
57076
57130
|
defaultValue,
|
|
@@ -57446,7 +57500,7 @@ const ControlGroup = props => {
|
|
|
57446
57500
|
const CONTROL_GROUP_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
|
|
57447
57501
|
|
|
57448
57502
|
installImportMetaCssBuild(import.meta);
|
|
57449
|
-
const css$
|
|
57503
|
+
const css$O = /* css */`@layer navi {
|
|
57450
57504
|
.navi_checkbox {
|
|
57451
57505
|
--switch-margin: 0;
|
|
57452
57506
|
--switch-padding: 2px;
|
|
@@ -57503,7 +57557,7 @@ const css$N = /* css */`@layer navi {
|
|
|
57503
57557
|
}
|
|
57504
57558
|
`;
|
|
57505
57559
|
const SwitchUI = () => {
|
|
57506
|
-
import.meta.css = [css$
|
|
57560
|
+
import.meta.css = [css$O, "@jsenv/navi/src/control/input/switch_ui.jsx"];
|
|
57507
57561
|
return jsx(Box, {
|
|
57508
57562
|
className: "navi_switch",
|
|
57509
57563
|
as: "svg",
|
|
@@ -57546,7 +57600,7 @@ const useCheckableProps = (props, options) => {
|
|
|
57546
57600
|
};
|
|
57547
57601
|
|
|
57548
57602
|
installImportMetaCssBuild(import.meta);
|
|
57549
|
-
const css$
|
|
57603
|
+
const css$N = /* css */`@layer navi {
|
|
57550
57604
|
.navi_checkbox {
|
|
57551
57605
|
--border-radius: var(--navi-checkbox-border-radius);
|
|
57552
57606
|
--border-width: var(--navi-control-border-width);
|
|
@@ -57819,7 +57873,7 @@ const InputCheckboxHeadless = props => {
|
|
|
57819
57873
|
});
|
|
57820
57874
|
};
|
|
57821
57875
|
const InputCheckboxFieldInterface = props => {
|
|
57822
|
-
import.meta.css = [css$
|
|
57876
|
+
import.meta.css = [css$N, "@jsenv/navi/src/control/input/input_checkbox.jsx"];
|
|
57823
57877
|
const [checkboxRootProps, checkboxHostProps] = useCheckableProps(props);
|
|
57824
57878
|
const {
|
|
57825
57879
|
icon,
|
|
@@ -57941,7 +57995,7 @@ const CheckboxPseudoClasses = [":hover", ":active", ":focus", ":focus-visible",
|
|
|
57941
57995
|
const CheckboxPseudoElements = ["::-navi-loader", "::-navi-checkmark"];
|
|
57942
57996
|
|
|
57943
57997
|
installImportMetaCssBuild(import.meta);
|
|
57944
|
-
const css$
|
|
57998
|
+
const css$M = /* css */`@layer navi {
|
|
57945
57999
|
.navi_label {
|
|
57946
58000
|
--label-required-indicator-color: var(--navi-color-danger, #b42318);
|
|
57947
58001
|
|
|
@@ -58019,7 +58073,7 @@ const css$L = /* css */`@layer navi {
|
|
|
58019
58073
|
* </Field>
|
|
58020
58074
|
*/
|
|
58021
58075
|
const Field = props => {
|
|
58022
|
-
import.meta.css = [css$
|
|
58076
|
+
import.meta.css = [css$M, "@jsenv/navi/src/control/field.jsx"];
|
|
58023
58077
|
const refDefault = useRef();
|
|
58024
58078
|
props.ref = props.ref || refDefault;
|
|
58025
58079
|
const {
|
|
@@ -58054,7 +58108,7 @@ const FieldCSSVars = {
|
|
|
58054
58108
|
spacingWithControl: ["--spacing-with-control", "padding"]
|
|
58055
58109
|
};
|
|
58056
58110
|
const FieldAsContainer = props => {
|
|
58057
|
-
import.meta.css = [css$
|
|
58111
|
+
import.meta.css = [css$M, "@jsenv/navi/src/control/field.jsx"];
|
|
58058
58112
|
const {
|
|
58059
58113
|
children
|
|
58060
58114
|
} = props;
|
|
@@ -58085,7 +58139,7 @@ const FieldAsContainer = props => {
|
|
|
58085
58139
|
};
|
|
58086
58140
|
const FIELD_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
|
|
58087
58141
|
const Label = props => {
|
|
58088
|
-
import.meta.css = [css$
|
|
58142
|
+
import.meta.css = [css$M, "@jsenv/navi/src/control/field.jsx"];
|
|
58089
58143
|
const {
|
|
58090
58144
|
children,
|
|
58091
58145
|
// Marks the label when its control is required. Takes what to show, or
|
|
@@ -58253,7 +58307,7 @@ const InputSlot = ({
|
|
|
58253
58307
|
};
|
|
58254
58308
|
|
|
58255
58309
|
installImportMetaCssBuild(import.meta);
|
|
58256
|
-
const css$
|
|
58310
|
+
const css$L = /* css */`@layer navi {
|
|
58257
58311
|
.navi_radio {
|
|
58258
58312
|
--margin: 3px 3px 3px 5px;
|
|
58259
58313
|
--outline-offset: 1px;
|
|
@@ -58559,7 +58613,7 @@ const InputRadioHeadless = props => {
|
|
|
58559
58613
|
};
|
|
58560
58614
|
const VARIANT_SET = new Set(["icon", "button", "radio"]);
|
|
58561
58615
|
const InputRadioFieldInterface = props => {
|
|
58562
|
-
import.meta.css = [css$
|
|
58616
|
+
import.meta.css = [css$L, "@jsenv/navi/src/control/input/input_radio.jsx"];
|
|
58563
58617
|
const [radioRootProps, radioHostProps] = useCheckableProps(props);
|
|
58564
58618
|
const {
|
|
58565
58619
|
icon,
|
|
@@ -58705,7 +58759,7 @@ const RadioPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":r
|
|
|
58705
58759
|
const RadioPseudoElements = ["::-navi-loader", "::-navi-radiomark"];
|
|
58706
58760
|
|
|
58707
58761
|
installImportMetaCssBuild(import.meta);
|
|
58708
|
-
const css$
|
|
58762
|
+
const css$K = /* css */`@layer navi {
|
|
58709
58763
|
.navi_input_range {
|
|
58710
58764
|
--border-radius: 6px;
|
|
58711
58765
|
--border-width: var(--navi-control-border-width);
|
|
@@ -58944,7 +58998,7 @@ const InputRange = props => {
|
|
|
58944
58998
|
});
|
|
58945
58999
|
};
|
|
58946
59000
|
const InputRangeFieldInterface = props => {
|
|
58947
|
-
import.meta.css = [css$
|
|
59001
|
+
import.meta.css = [css$K, "@jsenv/navi/src/control/input/input_range.jsx"];
|
|
58948
59002
|
const {
|
|
58949
59003
|
ref
|
|
58950
59004
|
} = props;
|
|
@@ -60453,7 +60507,7 @@ Input.UI = {
|
|
|
60453
60507
|
};
|
|
60454
60508
|
|
|
60455
60509
|
installImportMetaCssBuild(import.meta);
|
|
60456
|
-
const css$
|
|
60510
|
+
const css$J = /* css */`.navi_textarea {
|
|
60457
60511
|
& textarea {
|
|
60458
60512
|
min-height: calc(var(--textarea-min-rows, 1.5) * 1lh);
|
|
60459
60513
|
max-height: calc(var(--textarea-max-rows, 99999) * 1lh);
|
|
@@ -60540,7 +60594,7 @@ const Textarea = ({
|
|
|
60540
60594
|
...props
|
|
60541
60595
|
}) => {
|
|
60542
60596
|
installInputCss();
|
|
60543
|
-
import.meta.css = [css$
|
|
60597
|
+
import.meta.css = [css$J, "@jsenv/navi/src/control/input/textarea.jsx"];
|
|
60544
60598
|
const defaultRef = useRef(null);
|
|
60545
60599
|
props.ref = props.ref || defaultRef;
|
|
60546
60600
|
usePlaceholderHeight(props.ref, props.placeholder);
|
|
@@ -60622,7 +60676,7 @@ const TextareaCharCount = ({
|
|
|
60622
60676
|
maxLength,
|
|
60623
60677
|
...rest
|
|
60624
60678
|
}) => {
|
|
60625
|
-
import.meta.css = [css$
|
|
60679
|
+
import.meta.css = [css$J, "@jsenv/navi/src/control/input/textarea.jsx"];
|
|
60626
60680
|
const resolvedValue = signal ? signal.value : value;
|
|
60627
60681
|
const length = typeof resolvedValue === "string" ? resolvedValue.length : 0;
|
|
60628
60682
|
return jsx(Box, {
|
|
@@ -60732,7 +60786,7 @@ const RealTextarea = ({
|
|
|
60732
60786
|
};
|
|
60733
60787
|
|
|
60734
60788
|
installImportMetaCssBuild(import.meta);
|
|
60735
|
-
const css$
|
|
60789
|
+
const css$I = /* css */`.navi_editable_wrapper {
|
|
60736
60790
|
--inset-top: 0px;
|
|
60737
60791
|
--inset-right: 0px;
|
|
60738
60792
|
--inset-bottom: 0px;
|
|
@@ -60902,7 +60956,7 @@ const useEditionController = () => {
|
|
|
60902
60956
|
* Every other unknown prop goes to the field itself, styling props included.
|
|
60903
60957
|
*/
|
|
60904
60958
|
const Editable = props => {
|
|
60905
|
-
import.meta.css = [css$
|
|
60959
|
+
import.meta.css = [css$I, "@jsenv/navi/src/control/edition/editable.jsx"];
|
|
60906
60960
|
let {
|
|
60907
60961
|
children,
|
|
60908
60962
|
action,
|
|
@@ -61513,7 +61567,7 @@ const useRequestSubmitThroughGates = ref => {
|
|
|
61513
61567
|
// };
|
|
61514
61568
|
|
|
61515
61569
|
installImportMetaCssBuild(import.meta);
|
|
61516
|
-
const css$
|
|
61570
|
+
const css$H = /* css */`.navi_group {
|
|
61517
61571
|
--group-border-width: var(--navi-control-border-width);
|
|
61518
61572
|
|
|
61519
61573
|
& > :not([navi-out-of-flow]) {
|
|
@@ -61574,7 +61628,7 @@ const Group = ({
|
|
|
61574
61628
|
vertical = row,
|
|
61575
61629
|
...props
|
|
61576
61630
|
}) => {
|
|
61577
|
-
import.meta.css = [css$
|
|
61631
|
+
import.meta.css = [css$H, "@jsenv/navi/src/control/group.jsx"];
|
|
61578
61632
|
return jsx(Box, {
|
|
61579
61633
|
baseClassName: "navi_group",
|
|
61580
61634
|
"data-vertical": vertical ? "" : undefined
|
|
@@ -61706,7 +61760,7 @@ const unfreezeSize = (el) => {
|
|
|
61706
61760
|
};
|
|
61707
61761
|
|
|
61708
61762
|
installImportMetaCssBuild(import.meta);
|
|
61709
|
-
const css$
|
|
61763
|
+
const css$G = /* css */`@property --slide-travel-progress {
|
|
61710
61764
|
syntax: "<number>";
|
|
61711
61765
|
inherits: true;
|
|
61712
61766
|
initial-value: 0;
|
|
@@ -62193,7 +62247,7 @@ const SlideContainer = ({
|
|
|
62193
62247
|
children,
|
|
62194
62248
|
...rest
|
|
62195
62249
|
}) => {
|
|
62196
|
-
import.meta.css = [css$
|
|
62250
|
+
import.meta.css = [css$G, "@jsenv/navi/src/layout/slide_container.jsx"];
|
|
62197
62251
|
const debugFocus = useDebugFocus();
|
|
62198
62252
|
const trackRef = useRef();
|
|
62199
62253
|
// The box itself: it is what takes the keyboard when what is on screen holds
|
|
@@ -65544,6 +65598,1336 @@ const renderSafe = (value) => {
|
|
|
65544
65598
|
return String(value);
|
|
65545
65599
|
};
|
|
65546
65600
|
|
|
65601
|
+
installImportMetaCssBuild(import.meta);
|
|
65602
|
+
const PopupGroupContext = createContext(null);
|
|
65603
|
+
|
|
65604
|
+
/**
|
|
65605
|
+
* What a press outside a popup of the group does, read against a wall that is
|
|
65606
|
+
* always up: "ignore" closes, as "close" does. For a press the group does not
|
|
65607
|
+
* see itself, on a region the popup declared outside
|
|
65608
|
+
* (handlePressOnOutsideRegion).
|
|
65609
|
+
*/
|
|
65610
|
+
const readPressOutsideInPopupGroup = pressOutside => pressOutside === "ignore" ? "close" : pressOutside;
|
|
65611
|
+
|
|
65612
|
+
// The groups shown modal right now, for the popup around one of them to close
|
|
65613
|
+
// it first (see closePopupGroupsInside).
|
|
65614
|
+
const engagedGroupSet = new Set();
|
|
65615
|
+
|
|
65616
|
+
/**
|
|
65617
|
+
* Takes the groups engaged inside `popupElement` down, with what they have
|
|
65618
|
+
* open, before that popup gives the focus back: while a group is the topmost
|
|
65619
|
+
* modal everything else is inert, the element the focus goes back to
|
|
65620
|
+
* included. Called by Dialog and Popover as they close.
|
|
65621
|
+
*/
|
|
65622
|
+
const closePopupGroupsInside = (popupElement, closeEvent) => {
|
|
65623
|
+
for (const group of engagedGroupSet) {
|
|
65624
|
+
if (popupElement.contains(group.getElement())) {
|
|
65625
|
+
group.closeEverything(closeEvent);
|
|
65626
|
+
}
|
|
65627
|
+
}
|
|
65628
|
+
};
|
|
65629
|
+
const css$F = /* css */`@property --x-popup-group-top {
|
|
65630
|
+
syntax: "*";
|
|
65631
|
+
inherits: false
|
|
65632
|
+
}
|
|
65633
|
+
|
|
65634
|
+
@property --x-popup-group-left {
|
|
65635
|
+
syntax: "*";
|
|
65636
|
+
inherits: false
|
|
65637
|
+
}
|
|
65638
|
+
|
|
65639
|
+
@property --x-popup-group-width {
|
|
65640
|
+
syntax: "*";
|
|
65641
|
+
inherits: false
|
|
65642
|
+
}
|
|
65643
|
+
|
|
65644
|
+
@property --x-popup-group-height {
|
|
65645
|
+
syntax: "*";
|
|
65646
|
+
inherits: false
|
|
65647
|
+
}
|
|
65648
|
+
|
|
65649
|
+
@property --x-popup-group-padding {
|
|
65650
|
+
syntax: "*";
|
|
65651
|
+
inherits: false
|
|
65652
|
+
}
|
|
65653
|
+
|
|
65654
|
+
@property --x-popup-group-clip {
|
|
65655
|
+
syntax: "*";
|
|
65656
|
+
inherits: false
|
|
65657
|
+
}
|
|
65658
|
+
|
|
65659
|
+
@layer navi {
|
|
65660
|
+
.navi_popup_group {
|
|
65661
|
+
--backdrop-background: var(--navi-backdrop-close-background);
|
|
65662
|
+
--backdrop-filter: var(--navi-backdrop-close-backdrop-filter);
|
|
65663
|
+
width: auto;
|
|
65664
|
+
max-width: none;
|
|
65665
|
+
height: auto;
|
|
65666
|
+
max-height: none;
|
|
65667
|
+
color: inherit;
|
|
65668
|
+
background: none;
|
|
65669
|
+
border: none;
|
|
65670
|
+
margin: 0;
|
|
65671
|
+
padding: 0;
|
|
65672
|
+
display: flow-root;
|
|
65673
|
+
position: static;
|
|
65674
|
+
inset: auto;
|
|
65675
|
+
overflow: visible;
|
|
65676
|
+
}
|
|
65677
|
+
}
|
|
65678
|
+
|
|
65679
|
+
.navi_popup_group:modal {
|
|
65680
|
+
inset: auto !important;
|
|
65681
|
+
top: var(--x-popup-group-top) !important;
|
|
65682
|
+
left: var(--x-popup-group-left) !important;
|
|
65683
|
+
box-sizing: border-box !important;
|
|
65684
|
+
width: var(--x-popup-group-width) !important;
|
|
65685
|
+
min-width: 0 !important;
|
|
65686
|
+
max-width: none !important;
|
|
65687
|
+
height: var(--x-popup-group-height) !important;
|
|
65688
|
+
min-height: 0 !important;
|
|
65689
|
+
max-height: none !important;
|
|
65690
|
+
padding: var(--x-popup-group-padding) !important;
|
|
65691
|
+
clip-path: var(--x-popup-group-clip, none) !important;
|
|
65692
|
+
margin: 0 !important;
|
|
65693
|
+
position: fixed !important;
|
|
65694
|
+
}
|
|
65695
|
+
|
|
65696
|
+
.navi_popup_group[data-backdrop-variant="lift"] {
|
|
65697
|
+
--backdrop-background: var(--navi-backdrop-lift-background);
|
|
65698
|
+
--backdrop-filter: var(--navi-backdrop-lift-backdrop-filter);
|
|
65699
|
+
}
|
|
65700
|
+
|
|
65701
|
+
.navi_popup_group[data-backdrop-variant="discrete"] {
|
|
65702
|
+
--backdrop-background: var(--navi-backdrop-discrete-background);
|
|
65703
|
+
--backdrop-filter: var(--navi-backdrop-discrete-backdrop-filter);
|
|
65704
|
+
}
|
|
65705
|
+
|
|
65706
|
+
.navi_popup_group[data-backdrop-variant="invisible"] {
|
|
65707
|
+
--backdrop-background: transparent;
|
|
65708
|
+
--backdrop-filter: none;
|
|
65709
|
+
}
|
|
65710
|
+
|
|
65711
|
+
.navi_popup_group::backdrop {
|
|
65712
|
+
background: var(--backdrop-background);
|
|
65713
|
+
opacity: 1;
|
|
65714
|
+
backdrop-filter: var(--backdrop-filter);
|
|
65715
|
+
transition: opacity var(--popup-animation-duration, .18s) ease;
|
|
65716
|
+
|
|
65717
|
+
@starting-style {
|
|
65718
|
+
opacity: 0;
|
|
65719
|
+
}
|
|
65720
|
+
}
|
|
65721
|
+
|
|
65722
|
+
.navi_popup_group[navi-transitions-off]::backdrop {
|
|
65723
|
+
transition: none;
|
|
65724
|
+
}
|
|
65725
|
+
|
|
65726
|
+
:is(:root[data-navi-route-transition] .navi_popup_group:modal, :root[data-navi-route-travel] .navi_popup_group:modal) {
|
|
65727
|
+
view-transition-class: navi_popup;
|
|
65728
|
+
}
|
|
65729
|
+
|
|
65730
|
+
@supports (view-transition-name: match-element) {
|
|
65731
|
+
:root[data-navi-route-transition]:not([data-navi-route-transition-target="area"]) .navi_popup_group:modal {
|
|
65732
|
+
view-transition-name: match-element;
|
|
65733
|
+
}
|
|
65734
|
+
|
|
65735
|
+
:root[data-navi-route-travel] .navi_popup_group:modal {
|
|
65736
|
+
view-transition-name: match-element;
|
|
65737
|
+
}
|
|
65738
|
+
}
|
|
65739
|
+
`;
|
|
65740
|
+
|
|
65741
|
+
/**
|
|
65742
|
+
* The box around popups that hand over to each other: score cells, a menu bar,
|
|
65743
|
+
* a toolbar. Being inside it is what makes a popup and a button peers: no name
|
|
65744
|
+
* to give.
|
|
65745
|
+
*
|
|
65746
|
+
* While one of its popups is open, the group is shown modal where it stands:
|
|
65747
|
+
* what it holds stays lit and live, the rest of the screen is behind its wall,
|
|
65748
|
+
* and a press on another of its popups' triggers closes the open one and opens
|
|
65749
|
+
* that one in the same press. Its popups draw no wall of their own (their
|
|
65750
|
+
* backdrop props are not read), and a Dialog in it is never modal. The page
|
|
65751
|
+
* behind stays still while one of its Popovers or Dialogs is open; a callout
|
|
65752
|
+
* of it lets the page scroll, as a callout does anywhere.
|
|
65753
|
+
*
|
|
65754
|
+
* Its element is a `<dialog>`, closed at rest and laid out like any box: the
|
|
65755
|
+
* app styles it as it would a `<div>`. While engaged, an invisible placeholder
|
|
65756
|
+
* holds its place in the page, as the element before it, marked
|
|
65757
|
+
* `navi-out-of-flow` like everything navi adds to a tree. See
|
|
65758
|
+
* docs/popup_backdrop.md.
|
|
65759
|
+
*
|
|
65760
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
65761
|
+
* backdropVariant?: "auto" | "lift" | "discrete" | "invisible",
|
|
65762
|
+
* backdropColor?: string,
|
|
65763
|
+
* backdropFilter?: string,
|
|
65764
|
+
* themeColor?: string | false,
|
|
65765
|
+
* role?: string,
|
|
65766
|
+
* children?: import("ignore:preact").ComponentChildren,
|
|
65767
|
+
* [key: string]: any,
|
|
65768
|
+
* }>}
|
|
65769
|
+
* @param {"auto"|"lift"|"discrete"|"invisible"} [props.backdropVariant="auto"] -
|
|
65770
|
+
* How visible the wall is: `"auto"` is the wash a popup closing on an
|
|
65771
|
+
* outside press paints, `"lift"` the opaque blurred wall, `"discrete"` a
|
|
65772
|
+
* barely-there dim, `"invisible"` a wall that is not seen and still takes the
|
|
65773
|
+
* press.
|
|
65774
|
+
* @param {string} [props.backdropColor] - The wash itself, over the tokens.
|
|
65775
|
+
* @param {string} [props.backdropFilter] - What that wash does to what is
|
|
65776
|
+
* behind it.
|
|
65777
|
+
* @param {string|false} [props.themeColor] - The browser's colour while the
|
|
65778
|
+
* wall is up (see docs/popup_backdrop.md): worked out from the wash by
|
|
65779
|
+
* default.
|
|
65780
|
+
* @param {string} [props.role="group"] - A box of the page, not a dialog: the
|
|
65781
|
+
* element is a `<dialog>` for its modality alone.
|
|
65782
|
+
*/
|
|
65783
|
+
const PopupGroup = props => {
|
|
65784
|
+
import.meta.css = [css$F, "@jsenv/navi/src/layout/popup_group.jsx"];
|
|
65785
|
+
const {
|
|
65786
|
+
backdropVariant = "auto",
|
|
65787
|
+
backdropColor,
|
|
65788
|
+
backdropFilter,
|
|
65789
|
+
themeColor,
|
|
65790
|
+
role = "group",
|
|
65791
|
+
children,
|
|
65792
|
+
...rest
|
|
65793
|
+
} = props;
|
|
65794
|
+
const group = useMemo(createPopupGroup, []);
|
|
65795
|
+
group.themeColor = themeColor;
|
|
65796
|
+
// Unmounted or parked while engaged: a modal left open with its tree gone
|
|
65797
|
+
// keeps the whole document inert.
|
|
65798
|
+
useLayoutEffect(() => {
|
|
65799
|
+
return () => {
|
|
65800
|
+
group.disengage({
|
|
65801
|
+
leavingTree: true
|
|
65802
|
+
});
|
|
65803
|
+
};
|
|
65804
|
+
}, [group]);
|
|
65805
|
+
return jsx(Box, {
|
|
65806
|
+
as: "dialog",
|
|
65807
|
+
...rest,
|
|
65808
|
+
// A callback ignoring null: a Suspense park nulls an object ref and
|
|
65809
|
+
// gives it back only when its identity changes.
|
|
65810
|
+
ref: group.captureElement,
|
|
65811
|
+
baseClassName: "navi_popup_group",
|
|
65812
|
+
"navi-popup-group": "",
|
|
65813
|
+
role: role,
|
|
65814
|
+
"data-backdrop-variant": backdropVariant === "auto" ? undefined : backdropVariant,
|
|
65815
|
+
backdropColor: backdropColor,
|
|
65816
|
+
backdropFilter: backdropFilter,
|
|
65817
|
+
styleCSSVars: POPUP_GROUP_STYLE_CSS_VARS,
|
|
65818
|
+
children: jsx(PopupGroupContext.Provider, {
|
|
65819
|
+
value: group,
|
|
65820
|
+
children: children
|
|
65821
|
+
})
|
|
65822
|
+
});
|
|
65823
|
+
};
|
|
65824
|
+
const POPUP_GROUP_STYLE_CSS_VARS = {
|
|
65825
|
+
backdropColor: "--backdrop-background",
|
|
65826
|
+
backdropFilter: "--backdrop-filter"
|
|
65827
|
+
};
|
|
65828
|
+
|
|
65829
|
+
// How long after the release a press that closed a popup may still bring the
|
|
65830
|
+
// click that ends it: the compatibility events of a tap come after touchend,
|
|
65831
|
+
// in a task of their own.
|
|
65832
|
+
const END_OF_PRESS_SAFETY_MS = 1000;
|
|
65833
|
+
const createPopupGroup = () => {
|
|
65834
|
+
let groupEl = null;
|
|
65835
|
+
// Every popup rendered in the group, open or not: "is one of them open" is
|
|
65836
|
+
// their own decision, read before any of them shows (a lifting dialog shows a
|
|
65837
|
+
// frame after it decided).
|
|
65838
|
+
const memberControllerSet = new Set();
|
|
65839
|
+
// The popups of the group that are open, as their openEffect described
|
|
65840
|
+
// them, the last one to open last. One at a time, except when the open one
|
|
65841
|
+
// refuses to close as another opens (a value it cannot note): it is kept
|
|
65842
|
+
// here, so the group can still close it.
|
|
65843
|
+
const openMemberMap = new Map();
|
|
65844
|
+
let engagement = null;
|
|
65845
|
+
const noMemberOpen = () => {
|
|
65846
|
+
for (const openController of memberControllerSet) {
|
|
65847
|
+
if (openController.opened) {
|
|
65848
|
+
return false;
|
|
65849
|
+
}
|
|
65850
|
+
}
|
|
65851
|
+
return true;
|
|
65852
|
+
};
|
|
65853
|
+
const getOpenMembers = () => {
|
|
65854
|
+
const openMembers = [];
|
|
65855
|
+
for (const [openController, member] of openMemberMap) {
|
|
65856
|
+
if (openController.opened) {
|
|
65857
|
+
openMembers.push(member);
|
|
65858
|
+
} else {
|
|
65859
|
+
openMemberMap.delete(openController);
|
|
65860
|
+
}
|
|
65861
|
+
}
|
|
65862
|
+
return openMembers;
|
|
65863
|
+
};
|
|
65864
|
+
// The page behind is held still unless what is open is a callout, which
|
|
65865
|
+
// lets it scroll as it does anywhere (its closeByScroll answers that
|
|
65866
|
+
// scroll). Kept as it is while nothing is open: the wall stays up until the
|
|
65867
|
+
// press that closed the last popup ends.
|
|
65868
|
+
const followScrollLock = () => {
|
|
65869
|
+
const openMembers = getOpenMembers();
|
|
65870
|
+
if (openMembers.length === 0) {
|
|
65871
|
+
return;
|
|
65872
|
+
}
|
|
65873
|
+
engagement.followScrollLock(openMembers.some(openMember => !openMember.pageScrolls));
|
|
65874
|
+
};
|
|
65875
|
+
const group = {
|
|
65876
|
+
themeColor: undefined,
|
|
65877
|
+
captureElement: element => {
|
|
65878
|
+
if (element) {
|
|
65879
|
+
groupEl = element;
|
|
65880
|
+
}
|
|
65881
|
+
},
|
|
65882
|
+
getElement: () => groupEl,
|
|
65883
|
+
register: openController => {
|
|
65884
|
+
memberControllerSet.add(openController);
|
|
65885
|
+
return () => {
|
|
65886
|
+
memberControllerSet.delete(openController);
|
|
65887
|
+
};
|
|
65888
|
+
},
|
|
65889
|
+
/**
|
|
65890
|
+
* Called by a popup of the group as it opens, before it shows itself.
|
|
65891
|
+
* `member`: { openController, popupElement, pressOutside, event,
|
|
65892
|
+
* pageScrolls, popupId }. `popupElement` is read at each press, so a
|
|
65893
|
+
* member whose element is drawn after it engaged (a callout) passes a
|
|
65894
|
+
* getter. `pageScrolls`: the page behind may scroll while it is open.
|
|
65895
|
+
* `popupId`: the id a data-navi-popup-inside names it by, when it is not
|
|
65896
|
+
* the popup element's own.
|
|
65897
|
+
*/
|
|
65898
|
+
engage: member => {
|
|
65899
|
+
// One popup of the group at a time. A press on a peer has closed the
|
|
65900
|
+
// open one already (see armPressArbiter): this one opened from the
|
|
65901
|
+
// keyboard or from code.
|
|
65902
|
+
for (const openMember of getOpenMembers()) {
|
|
65903
|
+
if (openMember.openController === member.openController) {
|
|
65904
|
+
continue;
|
|
65905
|
+
}
|
|
65906
|
+
if (openMember.openController.requestClose(member.event)) {
|
|
65907
|
+
openMemberMap.delete(openMember.openController);
|
|
65908
|
+
}
|
|
65909
|
+
}
|
|
65910
|
+
openMemberMap.delete(member.openController);
|
|
65911
|
+
openMemberMap.set(member.openController, member);
|
|
65912
|
+
if (engagement) {
|
|
65913
|
+
engagement.cancelPendingCheck();
|
|
65914
|
+
} else {
|
|
65915
|
+
if (!groupEl || !groupEl.isConnected) {
|
|
65916
|
+
return;
|
|
65917
|
+
}
|
|
65918
|
+
engagement = engageGroup(groupEl, group, {
|
|
65919
|
+
silent: Boolean(member.event?.detail?.silent)
|
|
65920
|
+
});
|
|
65921
|
+
engagedGroupSet.add(group);
|
|
65922
|
+
}
|
|
65923
|
+
followScrollLock();
|
|
65924
|
+
},
|
|
65925
|
+
/**
|
|
65926
|
+
* Called by a popup of the group as it closes. Whether the group goes back
|
|
65927
|
+
* into the page is decided once nothing of the group is open — at the end
|
|
65928
|
+
* of the press that closed it when a press did, since that same press may
|
|
65929
|
+
* be opening a peer.
|
|
65930
|
+
*/
|
|
65931
|
+
release: (openController, closeEvent) => {
|
|
65932
|
+
openMemberMap.delete(openController);
|
|
65933
|
+
if (!engagement) {
|
|
65934
|
+
return;
|
|
65935
|
+
}
|
|
65936
|
+
followScrollLock();
|
|
65937
|
+
if (findEvent(closeEvent, ["pointerdown", "mousedown", "pointerup", "mouseup", "click"])) {
|
|
65938
|
+
if (findEvent(closeEvent, "click") || !engagement.pressInFlight) {
|
|
65939
|
+
// Closed from inside the click, or after the press ended (a lifting
|
|
65940
|
+
// Dialog closes in a view transition's update callback, a frame
|
|
65941
|
+
// later): what is left of the press is over once this task is.
|
|
65942
|
+
engagement.checkAfterTask();
|
|
65943
|
+
return;
|
|
65944
|
+
}
|
|
65945
|
+
engagement.pressClosed = true;
|
|
65946
|
+
return;
|
|
65947
|
+
}
|
|
65948
|
+
// Not a press (Escape, a command, code): a peer opening in the same
|
|
65949
|
+
// stack, from the keyboard, is the only thing to wait for.
|
|
65950
|
+
engagement.checkAfterMicrotask();
|
|
65951
|
+
},
|
|
65952
|
+
getOpenMember: () => {
|
|
65953
|
+
const openMembers = getOpenMembers();
|
|
65954
|
+
return openMembers.length ? openMembers[openMembers.length - 1] : null;
|
|
65955
|
+
},
|
|
65956
|
+
disengageIfNothingOpen: () => {
|
|
65957
|
+
if (engagement && noMemberOpen()) {
|
|
65958
|
+
group.disengage();
|
|
65959
|
+
}
|
|
65960
|
+
},
|
|
65961
|
+
// Nothing of the group may stay open over a page that is live again.
|
|
65962
|
+
closeEverything: closeEvent => {
|
|
65963
|
+
for (const openMember of getOpenMembers()) {
|
|
65964
|
+
openMember.openController.close(closeEvent);
|
|
65965
|
+
}
|
|
65966
|
+
openMemberMap.clear();
|
|
65967
|
+
group.disengage();
|
|
65968
|
+
},
|
|
65969
|
+
disengage: ({
|
|
65970
|
+
leavingTree = false
|
|
65971
|
+
} = {}) => {
|
|
65972
|
+
if (!engagement) {
|
|
65973
|
+
return;
|
|
65974
|
+
}
|
|
65975
|
+
const leavingEngagement = engagement;
|
|
65976
|
+
engagement = null;
|
|
65977
|
+
engagedGroupSet.delete(group);
|
|
65978
|
+
leavingEngagement.cleanup({
|
|
65979
|
+
leavingTree
|
|
65980
|
+
});
|
|
65981
|
+
}
|
|
65982
|
+
};
|
|
65983
|
+
return group;
|
|
65984
|
+
};
|
|
65985
|
+
const engageGroup = (groupEl, group, {
|
|
65986
|
+
silent
|
|
65987
|
+
}) => {
|
|
65988
|
+
// Everything read before anything is written.
|
|
65989
|
+
const rect = groupEl.getBoundingClientRect();
|
|
65990
|
+
const groupStyle = getComputedStyle(groupEl);
|
|
65991
|
+
// Copied out: the style is live, and shown modal the group has its margins
|
|
65992
|
+
// and its bounds forced.
|
|
65993
|
+
const {
|
|
65994
|
+
marginLeft,
|
|
65995
|
+
marginRight,
|
|
65996
|
+
minWidth,
|
|
65997
|
+
maxWidth
|
|
65998
|
+
} = groupStyle;
|
|
65999
|
+
const ancestorClip = readAncestorClip(groupEl, groupStyle);
|
|
66000
|
+
const baselineDescent = readBaselineDescent(groupEl, groupStyle, rect);
|
|
66001
|
+
const themeColor = readPopupThemeColor(groupEl, group.themeColor);
|
|
66002
|
+
const focusedBefore = document.activeElement;
|
|
66003
|
+
const focusWasInGroup = Boolean(focusedBefore) && focusedBefore !== groupEl && groupEl.contains(focusedBefore);
|
|
66004
|
+
// What the focus goes back to, when nothing holds it any more at the close.
|
|
66005
|
+
const focusOrigin = !focusWasInGroup && focusedBefore && focusedBefore !== document.body && focusedBefore !== groupEl ? focusedBefore : null;
|
|
66006
|
+
const ringBefore = Boolean(focusedBefore?.matches?.(":focus-visible"));
|
|
66007
|
+
const {
|
|
66008
|
+
scrollTop,
|
|
66009
|
+
scrollLeft
|
|
66010
|
+
} = groupEl;
|
|
66011
|
+
// A gesture on a box fixed in the top layer goes to the viewport: only a
|
|
66012
|
+
// place that scrolls with the viewport moves under it.
|
|
66013
|
+
const placeScrollsWithViewport = getScrollContainer(groupEl) === document.scrollingElement && !findSelfOrAncestorFixedPosition(groupEl);
|
|
66014
|
+
const placeholder = createPlaceholder(groupStyle, rect, baselineDescent);
|
|
66015
|
+
groupEl.before(placeholder);
|
|
66016
|
+
groupEl.style.setProperty("--x-popup-group-padding", `${groupStyle.paddingTop} ${groupStyle.paddingRight} ${groupStyle.paddingBottom} ${groupStyle.paddingLeft}`);
|
|
66017
|
+
const visibleBox = writePlacement(groupEl, rect, ancestorClip);
|
|
66018
|
+
if (silent) {
|
|
66019
|
+
groupEl.setAttribute("navi-transitions-off", "");
|
|
66020
|
+
}
|
|
66021
|
+
// showModal() runs the dialog focusing steps — it focuses the first element
|
|
66022
|
+
// marked autofocus, or else the first focusable the group holds, and
|
|
66023
|
+
// scrolls the group's own scroller to it — and records what held the focus,
|
|
66024
|
+
// for close() to give it back there. A native autofocus is otherwise never
|
|
66025
|
+
// used (see the focus skill): these exist for the length of the call, to
|
|
66026
|
+
// steer the browser's own pick, not to transfer anything.
|
|
66027
|
+
//
|
|
66028
|
+
// From inside the group the pick is what holds the focus already, any other
|
|
66029
|
+
// mark set aside, so nothing moves. From outside, a stop is put first in the
|
|
66030
|
+
// group and focused: it is what showModal() picks and records, and it is
|
|
66031
|
+
// gone by the close, which then moves nothing (see cleanup) — what the focus
|
|
66032
|
+
// came from, a sheet around the group, could not be kept from taking it
|
|
66033
|
+
// back. The focus goes on to the group itself, so no peer is focused on the
|
|
66034
|
+
// way.
|
|
66035
|
+
let focusStop = null;
|
|
66036
|
+
if (!focusWasInGroup) {
|
|
66037
|
+
focusStop = document.createElement("span");
|
|
66038
|
+
focusStop.setAttribute("navi-out-of-flow", "");
|
|
66039
|
+
focusStop.setAttribute("aria-hidden", "true");
|
|
66040
|
+
focusStop.tabIndex = -1;
|
|
66041
|
+
focusStop.style.cssText = "position: absolute; width: 0; height: 0; overflow: hidden";
|
|
66042
|
+
groupEl.prepend(focusStop);
|
|
66043
|
+
moveFocusTo(focusStop, {
|
|
66044
|
+
focusVisible: false
|
|
66045
|
+
});
|
|
66046
|
+
}
|
|
66047
|
+
const autofocusTarget = focusWasInGroup ? focusedBefore : focusStop;
|
|
66048
|
+
const autofocusWasSet = autofocusTarget.hasAttribute("autofocus");
|
|
66049
|
+
const autofocusSetAside = [...groupEl.querySelectorAll("[autofocus]")].filter(element => element !== autofocusTarget);
|
|
66050
|
+
for (const element of autofocusSetAside) {
|
|
66051
|
+
element.removeAttribute("autofocus");
|
|
66052
|
+
}
|
|
66053
|
+
if (!autofocusWasSet) {
|
|
66054
|
+
autofocusTarget.setAttribute("autofocus", "");
|
|
66055
|
+
}
|
|
66056
|
+
// Chromium takes a <dialog> without a tabindex for no focus target. Lent
|
|
66057
|
+
// for the whole engagement: taken back from the group holding the focus,
|
|
66058
|
+
// it would lose it.
|
|
66059
|
+
const tabindexLent = Boolean(focusStop) && !groupEl.hasAttribute("tabindex");
|
|
66060
|
+
try {
|
|
66061
|
+
groupEl.showModal();
|
|
66062
|
+
} catch (error) {
|
|
66063
|
+
focusStop?.remove();
|
|
66064
|
+
placeholder.remove();
|
|
66065
|
+
clearPlacement(groupEl);
|
|
66066
|
+
groupEl.removeAttribute("navi-transitions-off");
|
|
66067
|
+
throw error;
|
|
66068
|
+
} finally {
|
|
66069
|
+
if (!autofocusWasSet) {
|
|
66070
|
+
autofocusTarget.removeAttribute("autofocus");
|
|
66071
|
+
}
|
|
66072
|
+
for (const element of autofocusSetAside) {
|
|
66073
|
+
element.setAttribute("autofocus", "");
|
|
66074
|
+
}
|
|
66075
|
+
}
|
|
66076
|
+
if (focusStop) {
|
|
66077
|
+
if (tabindexLent) {
|
|
66078
|
+
groupEl.tabIndex = -1;
|
|
66079
|
+
}
|
|
66080
|
+
moveFocusTo(groupEl, {
|
|
66081
|
+
focusVisible: false
|
|
66082
|
+
});
|
|
66083
|
+
focusStop.remove();
|
|
66084
|
+
}
|
|
66085
|
+
// Measured once the group has left the flow, where a parent sized by its
|
|
66086
|
+
// content no longer counts it. Only a box given its width by what holds it
|
|
66087
|
+
// goes on following that width (a rotation while engaged); one sized by its
|
|
66088
|
+
// content, bounded by a min or max width the placeholder does not carry, or
|
|
66089
|
+
// placed by margins read here as px (auto ones, percentages), is held at
|
|
66090
|
+
// the width it has.
|
|
66091
|
+
const placeholderWidth = placeholder.getBoundingClientRect().width;
|
|
66092
|
+
if (parseFloat(marginLeft) !== 0 || parseFloat(marginRight) !== 0 || maxWidth !== "none" || minWidth !== "auto" && parseFloat(minWidth) !== 0 || placeholderWidth < rect.width - 0.5 || placeholderWidth > rect.width + 0.5) {
|
|
66093
|
+
placeholder.style.width = `${rect.width}px`;
|
|
66094
|
+
}
|
|
66095
|
+
// Where the engine focused something else anyway.
|
|
66096
|
+
if (groupEl.scrollTop !== scrollTop) {
|
|
66097
|
+
groupEl.scrollTop = scrollTop;
|
|
66098
|
+
}
|
|
66099
|
+
if (groupEl.scrollLeft !== scrollLeft) {
|
|
66100
|
+
groupEl.scrollLeft = scrollLeft;
|
|
66101
|
+
}
|
|
66102
|
+
if (focusWasInGroup && document.activeElement !== focusedBefore) {
|
|
66103
|
+
moveFocusTo(focusedBefore, {
|
|
66104
|
+
focusVisible: ringBefore
|
|
66105
|
+
});
|
|
66106
|
+
}
|
|
66107
|
+
if (silent) {
|
|
66108
|
+
// The wall's starting style is resolved with no transition to play from
|
|
66109
|
+
// it; giving the transition back afterwards changes no value, so nothing
|
|
66110
|
+
// fades.
|
|
66111
|
+
groupEl.getBoundingClientRect();
|
|
66112
|
+
groupEl.removeAttribute("navi-transitions-off");
|
|
66113
|
+
}
|
|
66114
|
+
// Top-layer order is the order things were shown in: whatever was already
|
|
66115
|
+
// open inside the group (a callout on a peer) is now under it, and goes back
|
|
66116
|
+
// on top.
|
|
66117
|
+
for (const topLayerEl of groupEl.querySelectorAll(":popover-open")) {
|
|
66118
|
+
topLayerEl.hidePopover();
|
|
66119
|
+
topLayerEl.showPopover();
|
|
66120
|
+
}
|
|
66121
|
+
const [cleanups, addCleanup] = createCleanupList();
|
|
66122
|
+
const engagement = {
|
|
66123
|
+
// From a press's pointerdown to its click (or its pointercancel).
|
|
66124
|
+
pressInFlight: false,
|
|
66125
|
+
// A popup closed during the press in flight: its end decides.
|
|
66126
|
+
pressClosed: false,
|
|
66127
|
+
visibleBox,
|
|
66128
|
+
cleanup: ({
|
|
66129
|
+
leavingTree
|
|
66130
|
+
}) => {
|
|
66131
|
+
for (const cleanup of cleanups) {
|
|
66132
|
+
cleanup();
|
|
66133
|
+
}
|
|
66134
|
+
clearPlacement(groupEl);
|
|
66135
|
+
placeholder.remove();
|
|
66136
|
+
if (leavingTree) {
|
|
66137
|
+
// Closed once the commit is over, as a Dialog leaving the tree is (see
|
|
66138
|
+
// its close cleanup). A park takes the group out of the top layer and
|
|
66139
|
+
// leaves its `open` attribute, which the reveal would show.
|
|
66140
|
+
queueMicrotask(() => {
|
|
66141
|
+
if (groupEl.open) {
|
|
66142
|
+
groupEl.close();
|
|
66143
|
+
}
|
|
66144
|
+
if (tabindexLent) {
|
|
66145
|
+
groupEl.removeAttribute("tabindex");
|
|
66146
|
+
}
|
|
66147
|
+
});
|
|
66148
|
+
if (focusOrigin) {
|
|
66149
|
+
// The focus left with the group: back where it was, if that stays.
|
|
66150
|
+
giveFocusBackAfterCommit(focusOrigin, () => {
|
|
66151
|
+
moveFocusTo(focusOrigin, {
|
|
66152
|
+
focusVisible: ringBefore
|
|
66153
|
+
});
|
|
66154
|
+
});
|
|
66155
|
+
}
|
|
66156
|
+
return;
|
|
66157
|
+
}
|
|
66158
|
+
const focusedBeforeClose = document.activeElement;
|
|
66159
|
+
if (!focusWasInGroup) {
|
|
66160
|
+
// What showModal() recorded is the stop, gone by now: close() moves
|
|
66161
|
+
// nothing, and the focus stays where a popup of the group, or the
|
|
66162
|
+
// user, put it. Only one still on the group, or lost, goes back to
|
|
66163
|
+
// where it came from.
|
|
66164
|
+
if (groupEl.open) {
|
|
66165
|
+
groupEl.close();
|
|
66166
|
+
}
|
|
66167
|
+
if (focusOrigin && focusOrigin.isConnected && (!focusedBeforeClose || focusedBeforeClose === document.body || focusedBeforeClose === groupEl)) {
|
|
66168
|
+
moveFocusTo(focusOrigin, {
|
|
66169
|
+
focusVisible: ringBefore
|
|
66170
|
+
});
|
|
66171
|
+
}
|
|
66172
|
+
if (tabindexLent) {
|
|
66173
|
+
groupEl.removeAttribute("tabindex");
|
|
66174
|
+
}
|
|
66175
|
+
return;
|
|
66176
|
+
}
|
|
66177
|
+
// Coming from inside the group, the focus was recorded where it was,
|
|
66178
|
+
// and close() gives it back there, undoing where a popup of the group,
|
|
66179
|
+
// or the user, put it since. Unless nothing moved it, that element is
|
|
66180
|
+
// made no focus target for the call: inert — or, holding the focus
|
|
66181
|
+
// inside it, without its tabindex, inert taking the focus with it.
|
|
66182
|
+
const ringBeforeClose = Boolean(focusedBeforeClose?.matches?.(":focus-visible"));
|
|
66183
|
+
const focusMovedInGroup = focusWasInGroup && focusedBeforeClose && focusedBeforeClose !== document.body && focusedBeforeClose !== groupEl && focusedBeforeClose !== focusedBefore && focusedBeforeClose.isConnected && focusedBefore.isConnected;
|
|
66184
|
+
const holdsFocusInside = focusMovedInGroup && focusedBefore.contains(focusedBeforeClose);
|
|
66185
|
+
const madeInert = focusMovedInGroup && !holdsFocusInside && !focusedBefore.inert;
|
|
66186
|
+
const tabindexSetAside = holdsFocusInside ? focusedBefore.getAttribute("tabindex") : null;
|
|
66187
|
+
if (madeInert) {
|
|
66188
|
+
focusedBefore.inert = true;
|
|
66189
|
+
}
|
|
66190
|
+
if (tabindexSetAside !== null) {
|
|
66191
|
+
focusedBefore.removeAttribute("tabindex");
|
|
66192
|
+
}
|
|
66193
|
+
try {
|
|
66194
|
+
if (groupEl.open) {
|
|
66195
|
+
groupEl.close();
|
|
66196
|
+
}
|
|
66197
|
+
} finally {
|
|
66198
|
+
if (madeInert) {
|
|
66199
|
+
focusedBefore.inert = false;
|
|
66200
|
+
}
|
|
66201
|
+
if (tabindexSetAside !== null) {
|
|
66202
|
+
focusedBefore.setAttribute("tabindex", tabindexSetAside);
|
|
66203
|
+
}
|
|
66204
|
+
if (tabindexLent) {
|
|
66205
|
+
groupEl.removeAttribute("tabindex");
|
|
66206
|
+
}
|
|
66207
|
+
}
|
|
66208
|
+
// Where the engine moved it anyway: an element focusable without a
|
|
66209
|
+
// tabindex.
|
|
66210
|
+
if (focusMovedInGroup && document.activeElement !== focusedBeforeClose) {
|
|
66211
|
+
moveFocusTo(focusedBeforeClose, {
|
|
66212
|
+
focusVisible: ringBeforeClose
|
|
66213
|
+
});
|
|
66214
|
+
}
|
|
66215
|
+
}
|
|
66216
|
+
};
|
|
66217
|
+
let pendingCheck = null;
|
|
66218
|
+
engagement.cancelPendingCheck = () => {
|
|
66219
|
+
if (pendingCheck) {
|
|
66220
|
+
pendingCheck.cancel();
|
|
66221
|
+
pendingCheck = null;
|
|
66222
|
+
}
|
|
66223
|
+
};
|
|
66224
|
+
engagement.checkAfterTask = (delay = 0) => {
|
|
66225
|
+
engagement.cancelPendingCheck();
|
|
66226
|
+
const timeout = setTimeout(() => {
|
|
66227
|
+
pendingCheck = null;
|
|
66228
|
+
group.disengageIfNothingOpen();
|
|
66229
|
+
}, delay);
|
|
66230
|
+
pendingCheck = {
|
|
66231
|
+
cancel: () => {
|
|
66232
|
+
clearTimeout(timeout);
|
|
66233
|
+
}
|
|
66234
|
+
};
|
|
66235
|
+
};
|
|
66236
|
+
engagement.checkAfterMicrotask = () => {
|
|
66237
|
+
engagement.cancelPendingCheck();
|
|
66238
|
+
let cancelled = false;
|
|
66239
|
+
queueMicrotask(() => {
|
|
66240
|
+
if (cancelled) {
|
|
66241
|
+
return;
|
|
66242
|
+
}
|
|
66243
|
+
pendingCheck = null;
|
|
66244
|
+
group.disengageIfNothingOpen();
|
|
66245
|
+
});
|
|
66246
|
+
pendingCheck = {
|
|
66247
|
+
cancel: () => {
|
|
66248
|
+
cancelled = true;
|
|
66249
|
+
}
|
|
66250
|
+
};
|
|
66251
|
+
};
|
|
66252
|
+
addCleanup(engagement.cancelPendingCheck);
|
|
66253
|
+
addCleanup(armPressArbiter(groupEl, group, engagement));
|
|
66254
|
+
addCleanup(followPlaceholder(groupEl, placeholder, engagement));
|
|
66255
|
+
// A wheel or a finger on the wall would scroll the page behind, and with it
|
|
66256
|
+
// the place the group is pinned to. On the wall means outside what is shown
|
|
66257
|
+
// of the group, which a clip can make less than its box. Let go for a
|
|
66258
|
+
// callout only where the group's place scrolls with the viewport: in a
|
|
66259
|
+
// sheet, a list scrolling on its own, a fixed bar, the gesture would scroll
|
|
66260
|
+
// the document behind and never the box the group stands in.
|
|
66261
|
+
let releaseScrollLock = null;
|
|
66262
|
+
const letGoOfScroll = () => {
|
|
66263
|
+
if (releaseScrollLock) {
|
|
66264
|
+
releaseScrollLock();
|
|
66265
|
+
releaseScrollLock = null;
|
|
66266
|
+
}
|
|
66267
|
+
};
|
|
66268
|
+
engagement.followScrollLock = openMemberHoldsPage => {
|
|
66269
|
+
if (!openMemberHoldsPage && placeScrollsWithViewport) {
|
|
66270
|
+
letGoOfScroll();
|
|
66271
|
+
return;
|
|
66272
|
+
}
|
|
66273
|
+
if (!releaseScrollLock) {
|
|
66274
|
+
releaseScrollLock = trapScrollInside(groupEl, {
|
|
66275
|
+
backdrop: true,
|
|
66276
|
+
getElementBox: () => engagement.visibleBox,
|
|
66277
|
+
// The group is no scroll container: shown fixed in the top layer, a
|
|
66278
|
+
// gesture over what it holds, or a scroll key pressed in it, would go
|
|
66279
|
+
// straight on to the page.
|
|
66280
|
+
containInside: true
|
|
66281
|
+
});
|
|
66282
|
+
}
|
|
66283
|
+
};
|
|
66284
|
+
addCleanup(letGoOfScroll);
|
|
66285
|
+
if (themeColor !== "none") {
|
|
66286
|
+
addCleanup(pushPopupThemeColor({
|
|
66287
|
+
themeColor,
|
|
66288
|
+
backdropColor: readBackdropColor(groupEl)
|
|
66289
|
+
}));
|
|
66290
|
+
}
|
|
66291
|
+
|
|
66292
|
+
// Escape and the back gesture reach the topmost modal, which is the group:
|
|
66293
|
+
// they are the open popup's to answer, as they would be with focus in it.
|
|
66294
|
+
// Escape is answered at its keydown, before it becomes the browser's close
|
|
66295
|
+
// request: a cancel can be prevented only while the page has a user
|
|
66296
|
+
// activation to spend, and Escape gives none — at the second one the
|
|
66297
|
+
// browser closes the group itself, whatever the open popup said. A keydown
|
|
66298
|
+
// prevented is no close request at all. Left to what the key was pressed in
|
|
66299
|
+
// when that uses it (a search field clearing itself), as the open popup's
|
|
66300
|
+
// own shortcut leaves it.
|
|
66301
|
+
const onKeyDown = keydownEvent => {
|
|
66302
|
+
if (keydownEvent.key !== "Escape" || keydownEvent.defaultPrevented) {
|
|
66303
|
+
// Answered already: by the open popup's own shortcut, when the focus is
|
|
66304
|
+
// in it.
|
|
66305
|
+
return;
|
|
66306
|
+
}
|
|
66307
|
+
if (keydownEvent.target !== groupEl) {
|
|
66308
|
+
const defaultAction = getKeyboardEventDefaultAction(keydownEvent);
|
|
66309
|
+
if (defaultAction && defaultAction !== "dismiss") {
|
|
66310
|
+
return;
|
|
66311
|
+
}
|
|
66312
|
+
}
|
|
66313
|
+
const openMember = group.getOpenMember();
|
|
66314
|
+
if (!openMember) {
|
|
66315
|
+
return;
|
|
66316
|
+
}
|
|
66317
|
+
// requestClose prevents the keydown, whether the popup closes or refuses.
|
|
66318
|
+
openMember.openController.requestClose(keydownEvent, {
|
|
66319
|
+
isCancel: true
|
|
66320
|
+
});
|
|
66321
|
+
};
|
|
66322
|
+
groupEl.addEventListener("keydown", onKeyDown);
|
|
66323
|
+
addCleanup(() => {
|
|
66324
|
+
groupEl.removeEventListener("keydown", onKeyDown);
|
|
66325
|
+
});
|
|
66326
|
+
// What still reaches the group as a close request: the back gesture, an
|
|
66327
|
+
// Escape with nothing of the group open.
|
|
66328
|
+
const onCancel = cancelEvent => {
|
|
66329
|
+
if (cancelEvent.target !== cancelEvent.currentTarget) {
|
|
66330
|
+
// A file input inside the group cancelling its own picker.
|
|
66331
|
+
return;
|
|
66332
|
+
}
|
|
66333
|
+
cancelEvent.preventDefault();
|
|
66334
|
+
const openMember = group.getOpenMember();
|
|
66335
|
+
if (openMember) {
|
|
66336
|
+
openMember.openController.requestClose(cancelEvent, {
|
|
66337
|
+
isCancel: true
|
|
66338
|
+
});
|
|
66339
|
+
return;
|
|
66340
|
+
}
|
|
66341
|
+
group.disengageIfNothingOpen();
|
|
66342
|
+
};
|
|
66343
|
+
groupEl.addEventListener("cancel", onCancel);
|
|
66344
|
+
addCleanup(() => {
|
|
66345
|
+
groupEl.removeEventListener("cancel", onCancel);
|
|
66346
|
+
});
|
|
66347
|
+
// The browser may close the group itself: a close request with no user
|
|
66348
|
+
// activation left to spend (a second back gesture) ignores preventDefault.
|
|
66349
|
+
const onNativeClose = closeEvent => {
|
|
66350
|
+
if (groupEl.open) {
|
|
66351
|
+
// Queued by an earlier engagement's close(): the group has been shown
|
|
66352
|
+
// modal again since, and nothing closed it.
|
|
66353
|
+
return;
|
|
66354
|
+
}
|
|
66355
|
+
group.closeEverything(closeEvent);
|
|
66356
|
+
};
|
|
66357
|
+
groupEl.addEventListener("close", onNativeClose);
|
|
66358
|
+
addCleanup(() => {
|
|
66359
|
+
groupEl.removeEventListener("close", onNativeClose);
|
|
66360
|
+
});
|
|
66361
|
+
// What holds the group closing (the sheet it is in) does not close the
|
|
66362
|
+
// group: a modal left open in a closed dialog keeps the whole document inert
|
|
66363
|
+
// with nothing on screen to dismiss. A navi popup closes the groups inside it
|
|
66364
|
+
// itself, before it gives the focus back (closePopupGroupsInside); this is
|
|
66365
|
+
// for any other.
|
|
66366
|
+
const openableAncestor = closestOpenableAncestor(groupEl);
|
|
66367
|
+
if (openableAncestor) {
|
|
66368
|
+
addCleanup(observeAncestorOpenState(openableAncestor, ({
|
|
66369
|
+
isOpen
|
|
66370
|
+
}) => {
|
|
66371
|
+
if (isOpen) {
|
|
66372
|
+
return;
|
|
66373
|
+
}
|
|
66374
|
+
group.closeEverything(new CustomEvent("popup_group_ancestor_close", {
|
|
66375
|
+
detail: {}
|
|
66376
|
+
}));
|
|
66377
|
+
}));
|
|
66378
|
+
}
|
|
66379
|
+
return engagement;
|
|
66380
|
+
};
|
|
66381
|
+
|
|
66382
|
+
/**
|
|
66383
|
+
* Hears every press while the group is engaged, for the popup of the group
|
|
66384
|
+
* that is open. A press outside that popup closes it, per its pressOutside,
|
|
66385
|
+
* and goes on to what it landed on: another popup's trigger opens it, a button
|
|
66386
|
+
* does its thing. A press on the wall — the ::backdrop reports the group as its
|
|
66387
|
+
* target, outside its box — closes and reaches nothing else, everything behind
|
|
66388
|
+
* being inert.
|
|
66389
|
+
*
|
|
66390
|
+
* Decided on the press's mousedown, the event the page acts on: on a touch
|
|
66391
|
+
* screen it is synthesized at touchend and hit-tested again, with the group
|
|
66392
|
+
* still up, so a finger that scrolled the group (pointercancel, no mousedown)
|
|
66393
|
+
* closes nothing. A pointerdown cancelled downstream (a drag source) brings no
|
|
66394
|
+
* mousedown: its click decides instead, or the start of the drag it turns
|
|
66395
|
+
* into, which brings no click. A popup that captures the outside is
|
|
66396
|
+
* answered at the pointerdown itself: nothing of that press may reach a peer.
|
|
66397
|
+
*/
|
|
66398
|
+
const armPressArbiter = (groupEl, group, engagement) => {
|
|
66399
|
+
let press = null;
|
|
66400
|
+
const isOnWall = event => {
|
|
66401
|
+
if (event.target !== groupEl) {
|
|
66402
|
+
return false;
|
|
66403
|
+
}
|
|
66404
|
+
const {
|
|
66405
|
+
left,
|
|
66406
|
+
top,
|
|
66407
|
+
right,
|
|
66408
|
+
bottom
|
|
66409
|
+
} = engagement.visibleBox;
|
|
66410
|
+
return event.clientX < left || event.clientX > right || event.clientY < top || event.clientY > bottom;
|
|
66411
|
+
};
|
|
66412
|
+
const spend = event => {
|
|
66413
|
+
event.preventDefault();
|
|
66414
|
+
event.stopImmediatePropagation();
|
|
66415
|
+
};
|
|
66416
|
+
// The open popup this press is outside of, or null.
|
|
66417
|
+
const findMemberPressedOutside = event => {
|
|
66418
|
+
const openMember = group.getOpenMember();
|
|
66419
|
+
if (!openMember) {
|
|
66420
|
+
return null;
|
|
66421
|
+
}
|
|
66422
|
+
if (openedDuringThisPress(openMember.openController)) {
|
|
66423
|
+
// The release of the press that opened it.
|
|
66424
|
+
return null;
|
|
66425
|
+
}
|
|
66426
|
+
const {
|
|
66427
|
+
target
|
|
66428
|
+
} = event;
|
|
66429
|
+
const {
|
|
66430
|
+
popupElement
|
|
66431
|
+
} = openMember;
|
|
66432
|
+
if (popupElement.contains(target)) {
|
|
66433
|
+
return null;
|
|
66434
|
+
}
|
|
66435
|
+
if (isDeclaredInsidePopup(target, openMember.popupId || popupElement.id)) {
|
|
66436
|
+
return null;
|
|
66437
|
+
}
|
|
66438
|
+
// In front of the open popup: something open inside the group that is
|
|
66439
|
+
// neither that popup nor around it (a callout on a peer).
|
|
66440
|
+
const layerUnderPointer = target.closest?.(':popover-open, dialog[open]:not([navi-popup-group]), [navi-control="popover"][aria-expanded="true"]');
|
|
66441
|
+
if (layerUnderPointer && !layerUnderPointer.contains(popupElement) && !popupElement.contains(layerUnderPointer)) {
|
|
66442
|
+
return null;
|
|
66443
|
+
}
|
|
66444
|
+
return openMember;
|
|
66445
|
+
};
|
|
66446
|
+
const readPressOutside = openMember => readPressOutsideInPopupGroup(openMember.pressOutside);
|
|
66447
|
+
const arbitrate = event => {
|
|
66448
|
+
const openMember = findMemberPressedOutside(event);
|
|
66449
|
+
if (!openMember) {
|
|
66450
|
+
return;
|
|
66451
|
+
}
|
|
66452
|
+
const pressOutside = readPressOutside(openMember);
|
|
66453
|
+
if (pressOutside === "capture") {
|
|
66454
|
+
spend(event);
|
|
66455
|
+
press.spendClick = true;
|
|
66456
|
+
return;
|
|
66457
|
+
}
|
|
66458
|
+
// A press on a peer goes on to it, so the close is told the pointerdown:
|
|
66459
|
+
// told the mousedown, the focus restore cancels that mousedown, and the
|
|
66460
|
+
// peer's own reaction to it with it (a picker opening at mousedown).
|
|
66461
|
+
let closeEvent;
|
|
66462
|
+
if (isOnWall(event)) {
|
|
66463
|
+
closeEvent = event;
|
|
66464
|
+
} else if (event.type === "click" && press.pointerdown) {
|
|
66465
|
+
// Decided at the click (the pointerdown was cancelled): the close is
|
|
66466
|
+
// told both, so what holds a reopen back for this press knows the press
|
|
66467
|
+
// ends with this very click.
|
|
66468
|
+
closeEvent = new CustomEvent("popup_group_press_outside", {
|
|
66469
|
+
detail: {
|
|
66470
|
+
event,
|
|
66471
|
+
eventChain: [press.pointerdown, event]
|
|
66472
|
+
}
|
|
66473
|
+
});
|
|
66474
|
+
} else {
|
|
66475
|
+
closeEvent = press.pointerdown || event;
|
|
66476
|
+
}
|
|
66477
|
+
const closed = openMember.openController.requestClose(closeEvent, {
|
|
66478
|
+
isCancel: pressOutside === "cancel"
|
|
66479
|
+
});
|
|
66480
|
+
if (!closed) {
|
|
66481
|
+
// The popup refuses to be left (a value it cannot note): the press must
|
|
66482
|
+
// not go on to open a peer next to it.
|
|
66483
|
+
spend(event);
|
|
66484
|
+
press.spendClick = true;
|
|
66485
|
+
}
|
|
66486
|
+
};
|
|
66487
|
+
const onPointerDown = pointerDownEvent => {
|
|
66488
|
+
if (engagement.pressClosed) {
|
|
66489
|
+
// The press that closed the last popup ended without a click (a gesture
|
|
66490
|
+
// swallowed it): this one is a new press.
|
|
66491
|
+
engagement.pressClosed = false;
|
|
66492
|
+
group.disengageIfNothingOpen();
|
|
66493
|
+
if (!groupEl.open) {
|
|
66494
|
+
return;
|
|
66495
|
+
}
|
|
66496
|
+
}
|
|
66497
|
+
press = {
|
|
66498
|
+
pointerdown: pointerDownEvent,
|
|
66499
|
+
mousedown: null,
|
|
66500
|
+
spendClick: false
|
|
66501
|
+
};
|
|
66502
|
+
if (pointerDownEvent.button !== 0) {
|
|
66503
|
+
return;
|
|
66504
|
+
}
|
|
66505
|
+
engagement.pressInFlight = true;
|
|
66506
|
+
const openMember = findMemberPressedOutside(pointerDownEvent);
|
|
66507
|
+
if (openMember && readPressOutside(openMember) === "capture") {
|
|
66508
|
+
// Cancelling the pointerdown also cancels the mousedown it would bring.
|
|
66509
|
+
spend(pointerDownEvent);
|
|
66510
|
+
press.spendClick = true;
|
|
66511
|
+
}
|
|
66512
|
+
};
|
|
66513
|
+
const onMouseDown = mouseDownEvent => {
|
|
66514
|
+
if (mouseDownEvent.button !== 0) {
|
|
66515
|
+
return;
|
|
66516
|
+
}
|
|
66517
|
+
if (!press) {
|
|
66518
|
+
press = {
|
|
66519
|
+
pointerdown: null,
|
|
66520
|
+
mousedown: null,
|
|
66521
|
+
spendClick: false
|
|
66522
|
+
};
|
|
66523
|
+
}
|
|
66524
|
+
engagement.pressInFlight = true;
|
|
66525
|
+
press.mousedown = mouseDownEvent;
|
|
66526
|
+
if (press.spendClick) {
|
|
66527
|
+
spend(mouseDownEvent);
|
|
66528
|
+
return;
|
|
66529
|
+
}
|
|
66530
|
+
arbitrate(mouseDownEvent);
|
|
66531
|
+
};
|
|
66532
|
+
const onClick = clickEvent => {
|
|
66533
|
+
if (!isPressDrivenClick(clickEvent)) {
|
|
66534
|
+
// A keyboard activation or a click() from code: not the click of the
|
|
66535
|
+
// press in flight, which the next pointerdown replaces anyway.
|
|
66536
|
+
return;
|
|
66537
|
+
}
|
|
66538
|
+
if (press?.spendClick) {
|
|
66539
|
+
spend(clickEvent);
|
|
66540
|
+
} else if (press && !press.mousedown) {
|
|
66541
|
+
arbitrate(clickEvent);
|
|
66542
|
+
}
|
|
66543
|
+
press = null;
|
|
66544
|
+
engagement.pressInFlight = false;
|
|
66545
|
+
if (engagement.pressClosed) {
|
|
66546
|
+
engagement.pressClosed = false;
|
|
66547
|
+
// After the click's dispatch: a peer it opens counts.
|
|
66548
|
+
engagement.checkAfterTask();
|
|
66549
|
+
}
|
|
66550
|
+
};
|
|
66551
|
+
const onPointerUp = () => {
|
|
66552
|
+
if (engagement.pressClosed) {
|
|
66553
|
+
engagement.checkAfterTask(END_OF_PRESS_SAFETY_MS);
|
|
66554
|
+
}
|
|
66555
|
+
};
|
|
66556
|
+
const onPointerCancel = () => {
|
|
66557
|
+
press = null;
|
|
66558
|
+
engagement.pressInFlight = false;
|
|
66559
|
+
if (engagement.pressClosed) {
|
|
66560
|
+
engagement.pressClosed = false;
|
|
66561
|
+
engagement.checkAfterTask();
|
|
66562
|
+
}
|
|
66563
|
+
};
|
|
66564
|
+
// A press that becomes a drag brings no click: decided at the click, it
|
|
66565
|
+
// would leave the open popup over a card being carried away, its own
|
|
66566
|
+
// trigger maybe. A drag starting outside the open popup closes it, as a
|
|
66567
|
+
// press there does. Nothing is left to hand over to then, so the group goes
|
|
66568
|
+
// back into the page at once — where the drag can land: what is outside the
|
|
66569
|
+
// group is inert while it is engaged.
|
|
66570
|
+
const onDragStart = dragStartEvent => {
|
|
66571
|
+
const openMember = findMemberPressedOutside(dragStartEvent);
|
|
66572
|
+
if (openMember) {
|
|
66573
|
+
const pressOutside = readPressOutside(openMember);
|
|
66574
|
+
if (pressOutside !== "capture") {
|
|
66575
|
+
openMember.openController.requestClose(dragStartEvent, {
|
|
66576
|
+
isCancel: pressOutside === "cancel"
|
|
66577
|
+
});
|
|
66578
|
+
}
|
|
66579
|
+
}
|
|
66580
|
+
if (engagement.pressClosed) {
|
|
66581
|
+
engagement.pressClosed = false;
|
|
66582
|
+
group.disengageIfNothingOpen();
|
|
66583
|
+
}
|
|
66584
|
+
};
|
|
66585
|
+
const listeners = [["pointerdown", onPointerDown], ["mousedown", onMouseDown], ["click", onClick], ["pointerup", onPointerUp], ["pointercancel", onPointerCancel], ["navi_drag_start", onDragStart]];
|
|
66586
|
+
for (const [type, listener] of listeners) {
|
|
66587
|
+
document.addEventListener(type, listener, {
|
|
66588
|
+
capture: true
|
|
66589
|
+
});
|
|
66590
|
+
}
|
|
66591
|
+
return () => {
|
|
66592
|
+
for (const [type, listener] of listeners) {
|
|
66593
|
+
document.removeEventListener(type, listener, {
|
|
66594
|
+
capture: true
|
|
66595
|
+
});
|
|
66596
|
+
}
|
|
66597
|
+
};
|
|
66598
|
+
};
|
|
66599
|
+
|
|
66600
|
+
// The group follows the place it holds in the page: a scroll of what holds it,
|
|
66601
|
+
// the viewport resizing (a keyboard, a rotation), the page reflowing around
|
|
66602
|
+
// it, a popup it is in being placed again. Its own scroll moves nothing.
|
|
66603
|
+
const followPlaceholder = (groupEl, placeholder, engagement) => {
|
|
66604
|
+
const [cleanups, addCleanup] = createCleanupList();
|
|
66605
|
+
// An ancestor's movement may end after this engagement did, and the group a
|
|
66606
|
+
// later one engaged is the same element.
|
|
66607
|
+
let disposed = false;
|
|
66608
|
+
addCleanup(() => {
|
|
66609
|
+
disposed = true;
|
|
66610
|
+
});
|
|
66611
|
+
const update = () => {
|
|
66612
|
+
if (disposed) {
|
|
66613
|
+
return;
|
|
66614
|
+
}
|
|
66615
|
+
const rect = placeholder.getBoundingClientRect();
|
|
66616
|
+
const ancestorClip = readAncestorClip(placeholder, getComputedStyle(placeholder));
|
|
66617
|
+
engagement.visibleBox = writePlacement(groupEl, rect, ancestorClip);
|
|
66618
|
+
};
|
|
66619
|
+
const onScroll = scrollEvent => {
|
|
66620
|
+
if (scrollEvent.target === groupEl || groupEl.contains(scrollEvent.target)) {
|
|
66621
|
+
return;
|
|
66622
|
+
}
|
|
66623
|
+
update();
|
|
66624
|
+
};
|
|
66625
|
+
document.addEventListener("scroll", onScroll, {
|
|
66626
|
+
capture: true,
|
|
66627
|
+
passive: true
|
|
66628
|
+
});
|
|
66629
|
+
window.addEventListener("resize", update);
|
|
66630
|
+
const {
|
|
66631
|
+
visualViewport
|
|
66632
|
+
} = window;
|
|
66633
|
+
visualViewport?.addEventListener("resize", update);
|
|
66634
|
+
addCleanup(() => {
|
|
66635
|
+
document.removeEventListener("scroll", onScroll, {
|
|
66636
|
+
capture: true
|
|
66637
|
+
});
|
|
66638
|
+
window.removeEventListener("resize", update);
|
|
66639
|
+
visualViewport?.removeEventListener("resize", update);
|
|
66640
|
+
});
|
|
66641
|
+
const resizeObserver = new ResizeObserver(update);
|
|
66642
|
+
if (placeholder.parentElement) {
|
|
66643
|
+
resizeObserver.observe(placeholder.parentElement);
|
|
66644
|
+
}
|
|
66645
|
+
addCleanup(() => {
|
|
66646
|
+
resizeObserver.disconnect();
|
|
66647
|
+
});
|
|
66648
|
+
// A popup the group is in moves by a translate, animated, and says so only
|
|
66649
|
+
// to itself (applyNewPosition): followed frame by frame while it moves, as
|
|
66650
|
+
// visibleRectEffect follows it for what is anchored inside it.
|
|
66651
|
+
let ancestor = closestOpenableAncestor(groupEl);
|
|
66652
|
+
while (ancestor) {
|
|
66653
|
+
const openableAncestor = ancestor;
|
|
66654
|
+
let positionTransitionFrame = null;
|
|
66655
|
+
const onPositionChange = () => {
|
|
66656
|
+
update();
|
|
66657
|
+
};
|
|
66658
|
+
const onPositionTransition = positionTransitionEvent => {
|
|
66659
|
+
cancelAnimationFrame(positionTransitionFrame);
|
|
66660
|
+
const loop = () => {
|
|
66661
|
+
update();
|
|
66662
|
+
positionTransitionFrame = requestAnimationFrame(loop);
|
|
66663
|
+
};
|
|
66664
|
+
loop();
|
|
66665
|
+
positionTransitionEvent.detail.onEnd(() => {
|
|
66666
|
+
cancelAnimationFrame(positionTransitionFrame);
|
|
66667
|
+
positionTransitionFrame = null;
|
|
66668
|
+
update();
|
|
66669
|
+
});
|
|
66670
|
+
};
|
|
66671
|
+
openableAncestor.addEventListener("navi_position_change", onPositionChange);
|
|
66672
|
+
openableAncestor.addEventListener("navi_position_transition", onPositionTransition);
|
|
66673
|
+
addCleanup(() => {
|
|
66674
|
+
cancelAnimationFrame(positionTransitionFrame);
|
|
66675
|
+
openableAncestor.removeEventListener("navi_position_change", onPositionChange);
|
|
66676
|
+
openableAncestor.removeEventListener("navi_position_transition", onPositionTransition);
|
|
66677
|
+
});
|
|
66678
|
+
ancestor = closestOpenableAncestor(openableAncestor);
|
|
66679
|
+
}
|
|
66680
|
+
return () => {
|
|
66681
|
+
for (const cleanup of cleanups) {
|
|
66682
|
+
cleanup();
|
|
66683
|
+
}
|
|
66684
|
+
};
|
|
66685
|
+
};
|
|
66686
|
+
|
|
66687
|
+
// What stands in the page for the group while it is in the top layer: the same
|
|
66688
|
+
// outer box at the height it had, invisible, counted by nothing that skips
|
|
66689
|
+
// what navi puts in a tree (css_architecture.md, "Counting children").
|
|
66690
|
+
const createPlaceholder = (groupStyle, rect, baselineDescent) => {
|
|
66691
|
+
const placeholder = document.createElement("div");
|
|
66692
|
+
placeholder.setAttribute("navi-popup-group-placeholder", "");
|
|
66693
|
+
placeholder.setAttribute("navi-out-of-flow", "");
|
|
66694
|
+
placeholder.setAttribute("aria-hidden", "true");
|
|
66695
|
+
const {
|
|
66696
|
+
style
|
|
66697
|
+
} = placeholder;
|
|
66698
|
+
if (groupStyle.display.startsWith("inline")) {
|
|
66699
|
+
style.display = "inline-block";
|
|
66700
|
+
} else if (establishesFormattingContext(groupStyle)) {
|
|
66701
|
+
// Such a box keeps clear of a float beside it, where a plain block slides
|
|
66702
|
+
// under it.
|
|
66703
|
+
style.display = "flow-root";
|
|
66704
|
+
} else {
|
|
66705
|
+
style.display = "block";
|
|
66706
|
+
}
|
|
66707
|
+
style.visibility = "hidden";
|
|
66708
|
+
style.pointerEvents = "none";
|
|
66709
|
+
style.boxSizing = "border-box";
|
|
66710
|
+
style.height = `${rect.height}px`;
|
|
66711
|
+
style.flex = "none";
|
|
66712
|
+
for (const property of PLACEHOLDER_COPIED_PROPERTIES) {
|
|
66713
|
+
style[property] = groupStyle[property];
|
|
66714
|
+
}
|
|
66715
|
+
if (baselineDescent !== null) {
|
|
66716
|
+
// An empty inline box sits on its bottom edge; the group sat on its own
|
|
66717
|
+
// baseline, this far above its bottom.
|
|
66718
|
+
style.verticalAlign = `${-baselineDescent}px`;
|
|
66719
|
+
}
|
|
66720
|
+
return placeholder;
|
|
66721
|
+
};
|
|
66722
|
+
const establishesFormattingContext = style => {
|
|
66723
|
+
const {
|
|
66724
|
+
display,
|
|
66725
|
+
overflowX,
|
|
66726
|
+
overflowY,
|
|
66727
|
+
contain,
|
|
66728
|
+
containerType,
|
|
66729
|
+
contentVisibility,
|
|
66730
|
+
columnCount,
|
|
66731
|
+
columnWidth
|
|
66732
|
+
} = style;
|
|
66733
|
+
if (display === "flow-root" || display === "flex" || display === "grid" || display === "table" || display.includes("flow-root")) {
|
|
66734
|
+
return true;
|
|
66735
|
+
}
|
|
66736
|
+
if (overflowX !== "visible" && overflowX !== "clip" || overflowY !== "visible" && overflowY !== "clip") {
|
|
66737
|
+
return true;
|
|
66738
|
+
}
|
|
66739
|
+
// Containment the computed `contain` does not show: a size container, a box
|
|
66740
|
+
// whose content-visibility skips its content.
|
|
66741
|
+
if (/\b(?:size|inline-size)\b/.test(containerType) || contentVisibility === "auto" || contentVisibility === "hidden") {
|
|
66742
|
+
return true;
|
|
66743
|
+
}
|
|
66744
|
+
// A multi-column box.
|
|
66745
|
+
if (columnCount !== "auto" || columnWidth !== "auto") {
|
|
66746
|
+
return true;
|
|
66747
|
+
}
|
|
66748
|
+
return /\b(?:layout|paint|strict|content)\b/.test(contain);
|
|
66749
|
+
};
|
|
66750
|
+
const PLACEHOLDER_COPIED_PROPERTIES = ["position", "top", "right", "bottom", "left", "marginTop", "marginRight", "marginBottom", "marginLeft", "alignSelf", "justifySelf", "order", "gridRowStart", "gridRowEnd", "gridColumnStart", "gridColumnEnd", "float", "verticalAlign"];
|
|
66751
|
+
|
|
66752
|
+
// How far below the line's baseline an inline-level group on that baseline
|
|
66753
|
+
// reaches: what the placeholder, which has no line of its own, has to sink by.
|
|
66754
|
+
const readBaselineDescent = (groupEl, groupStyle, rect) => {
|
|
66755
|
+
if (!groupStyle.display.startsWith("inline") || groupStyle.verticalAlign !== "baseline") {
|
|
66756
|
+
return null;
|
|
66757
|
+
}
|
|
66758
|
+
const marginTop = parseFloat(groupStyle.marginTop) || 0;
|
|
66759
|
+
const marginBottom = parseFloat(groupStyle.marginBottom) || 0;
|
|
66760
|
+
const readBaselineY = insert => {
|
|
66761
|
+
const probe = document.createElement("span");
|
|
66762
|
+
probe.style.cssText = "display:inline-block;width:0;height:0;vertical-align:baseline";
|
|
66763
|
+
insert(probe);
|
|
66764
|
+
const baselineY = probe.getBoundingClientRect().bottom;
|
|
66765
|
+
probe.remove();
|
|
66766
|
+
return baselineY;
|
|
66767
|
+
};
|
|
66768
|
+
// On the group's own line, between its top and its bottom margin edge: a
|
|
66769
|
+
// probe that takes no room can stay at the end of the line before it.
|
|
66770
|
+
const isOnGroupLine = y => y >= rect.top - marginTop - 0.5 && y <= rect.bottom + marginBottom + 0.5;
|
|
66771
|
+
let baselineY = readBaselineY(probe => groupEl.before(probe));
|
|
66772
|
+
if (!isOnGroupLine(baselineY)) {
|
|
66773
|
+
baselineY = readBaselineY(probe => groupEl.after(probe));
|
|
66774
|
+
if (!isOnGroupLine(baselineY)) {
|
|
66775
|
+
return null;
|
|
66776
|
+
}
|
|
66777
|
+
}
|
|
66778
|
+
// Down to the margin edge: that is what an empty inline box sits on.
|
|
66779
|
+
return rect.bottom + marginBottom - baselineY;
|
|
66780
|
+
};
|
|
66781
|
+
|
|
66782
|
+
// Writes where the group stands and how its ancestors clip it; returns what of
|
|
66783
|
+
// it is on screen.
|
|
66784
|
+
const writePlacement = (groupEl, rect, ancestorClip) => {
|
|
66785
|
+
const {
|
|
66786
|
+
style
|
|
66787
|
+
} = groupEl;
|
|
66788
|
+
style.setProperty("--x-popup-group-top", `${rect.top}px`);
|
|
66789
|
+
style.setProperty("--x-popup-group-left", `${rect.left}px`);
|
|
66790
|
+
style.setProperty("--x-popup-group-width", `${rect.width}px`);
|
|
66791
|
+
style.setProperty("--x-popup-group-height", `${rect.height}px`);
|
|
66792
|
+
if (ancestorClip) {
|
|
66793
|
+
// From the group's border box, reaching past it on a side nothing clips:
|
|
66794
|
+
// what overflows the group was painted up to its ancestors' edge.
|
|
66795
|
+
const toInset = distance => distance === -Infinity ? "-100vmax" : `${distance}px`;
|
|
66796
|
+
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)})`);
|
|
66797
|
+
} else {
|
|
66798
|
+
style.removeProperty("--x-popup-group-clip");
|
|
66799
|
+
}
|
|
66800
|
+
return intersectBox(rect, ancestorClip);
|
|
66801
|
+
};
|
|
66802
|
+
const clearPlacement = groupEl => {
|
|
66803
|
+
const {
|
|
66804
|
+
style
|
|
66805
|
+
} = groupEl;
|
|
66806
|
+
style.removeProperty("--x-popup-group-top");
|
|
66807
|
+
style.removeProperty("--x-popup-group-left");
|
|
66808
|
+
style.removeProperty("--x-popup-group-width");
|
|
66809
|
+
style.removeProperty("--x-popup-group-height");
|
|
66810
|
+
style.removeProperty("--x-popup-group-padding");
|
|
66811
|
+
style.removeProperty("--x-popup-group-clip");
|
|
66812
|
+
};
|
|
66813
|
+
|
|
66814
|
+
// The clip the ancestors of `element` put on it, up to the first box nothing
|
|
66815
|
+
// around clips (a top-layer or fixed one), -Infinity/Infinity on a side
|
|
66816
|
+
// nothing clips — or null when nothing clips it at all.
|
|
66817
|
+
const readAncestorClip = (element, elementStyle) => {
|
|
66818
|
+
if (elementStyle.position === "fixed") {
|
|
66819
|
+
return null;
|
|
66820
|
+
}
|
|
66821
|
+
let left = -Infinity;
|
|
66822
|
+
let top = -Infinity;
|
|
66823
|
+
let right = Infinity;
|
|
66824
|
+
let bottom = Infinity;
|
|
66825
|
+
let clipped = false;
|
|
66826
|
+
// An absolutely placed box escapes the clip of every ancestor between it
|
|
66827
|
+
// and its containing block: those are skipped.
|
|
66828
|
+
let skipUntilContainingBlock = elementStyle.position === "absolute";
|
|
66829
|
+
let ancestor = element.parentElement;
|
|
66830
|
+
while (ancestor && ancestor !== document.documentElement) {
|
|
66831
|
+
const ancestorStyle = getComputedStyle(ancestor);
|
|
66832
|
+
if (skipUntilContainingBlock && !isContainingBlockForAbsolute(ancestorStyle)) {
|
|
66833
|
+
ancestor = ancestor.parentElement;
|
|
66834
|
+
continue;
|
|
66835
|
+
}
|
|
66836
|
+
skipUntilContainingBlock = false;
|
|
66837
|
+
const {
|
|
66838
|
+
overflowX,
|
|
66839
|
+
overflowY,
|
|
66840
|
+
position
|
|
66841
|
+
} = ancestorStyle;
|
|
66842
|
+
// Read on a body whose overflow is the viewport's, it clips nothing.
|
|
66843
|
+
const clipsNothing = overflowGoesToViewport(ancestor);
|
|
66844
|
+
const clipsX = !clipsNothing && overflowX !== "visible";
|
|
66845
|
+
const clipsY = !clipsNothing && overflowY !== "visible";
|
|
66846
|
+
if (clipsX || clipsY) {
|
|
66847
|
+
clipped = true;
|
|
66848
|
+
const ancestorRect = ancestor.getBoundingClientRect();
|
|
66849
|
+
const innerLeft = ancestorRect.left + ancestor.clientLeft;
|
|
66850
|
+
const innerTop = ancestorRect.top + ancestor.clientTop;
|
|
66851
|
+
if (clipsX) {
|
|
66852
|
+
const innerRight = innerLeft + ancestor.clientWidth;
|
|
66853
|
+
if (innerLeft > left) {
|
|
66854
|
+
left = innerLeft;
|
|
66855
|
+
}
|
|
66856
|
+
if (innerRight < right) {
|
|
66857
|
+
right = innerRight;
|
|
66858
|
+
}
|
|
66859
|
+
}
|
|
66860
|
+
if (clipsY) {
|
|
66861
|
+
const innerBottom = innerTop + ancestor.clientHeight;
|
|
66862
|
+
if (innerTop > top) {
|
|
66863
|
+
top = innerTop;
|
|
66864
|
+
}
|
|
66865
|
+
if (innerBottom < bottom) {
|
|
66866
|
+
bottom = innerBottom;
|
|
66867
|
+
}
|
|
66868
|
+
}
|
|
66869
|
+
}
|
|
66870
|
+
if (position === "fixed" || ancestor.matches(":popover-open, dialog:modal")) {
|
|
66871
|
+
break;
|
|
66872
|
+
}
|
|
66873
|
+
if (position === "absolute") {
|
|
66874
|
+
skipUntilContainingBlock = true;
|
|
66875
|
+
}
|
|
66876
|
+
ancestor = ancestor.parentElement;
|
|
66877
|
+
}
|
|
66878
|
+
return clipped ? {
|
|
66879
|
+
left,
|
|
66880
|
+
top,
|
|
66881
|
+
right,
|
|
66882
|
+
bottom
|
|
66883
|
+
} : null;
|
|
66884
|
+
};
|
|
66885
|
+
const isContainingBlockForAbsolute = style => style.position !== "static" || style.transform !== "none" || style.translate !== "none" || style.rotate !== "none" || style.scale !== "none" || style.perspective !== "none" || style.filter !== "none" ||
|
|
66886
|
+
// Prefixed alone before Safari 18.
|
|
66887
|
+
(style.backdropFilter ?? style.webkitBackdropFilter ?? "none") !== "none" || /\b(?:layout|paint|strict|content)\b/.test(style.contain) ||
|
|
66888
|
+
// Layout containment the computed `contain` does not show.
|
|
66889
|
+
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);
|
|
66890
|
+
const intersectBox = (rect, clip) => {
|
|
66891
|
+
let {
|
|
66892
|
+
left,
|
|
66893
|
+
top,
|
|
66894
|
+
right,
|
|
66895
|
+
bottom
|
|
66896
|
+
} = rect;
|
|
66897
|
+
if (clip) {
|
|
66898
|
+
if (clip.left > left) {
|
|
66899
|
+
left = clip.left;
|
|
66900
|
+
}
|
|
66901
|
+
if (clip.top > top) {
|
|
66902
|
+
top = clip.top;
|
|
66903
|
+
}
|
|
66904
|
+
if (clip.right < right) {
|
|
66905
|
+
right = clip.right;
|
|
66906
|
+
}
|
|
66907
|
+
if (clip.bottom < bottom) {
|
|
66908
|
+
bottom = clip.bottom;
|
|
66909
|
+
}
|
|
66910
|
+
}
|
|
66911
|
+
if (right < left) {
|
|
66912
|
+
right = left;
|
|
66913
|
+
}
|
|
66914
|
+
if (bottom < top) {
|
|
66915
|
+
bottom = top;
|
|
66916
|
+
}
|
|
66917
|
+
return {
|
|
66918
|
+
left,
|
|
66919
|
+
top,
|
|
66920
|
+
right,
|
|
66921
|
+
bottom
|
|
66922
|
+
};
|
|
66923
|
+
};
|
|
66924
|
+
const createCleanupList = () => {
|
|
66925
|
+
const cleanups = [];
|
|
66926
|
+
return [cleanups, cleanup => {
|
|
66927
|
+
cleanups.push(cleanup);
|
|
66928
|
+
}];
|
|
66929
|
+
};
|
|
66930
|
+
|
|
65547
66931
|
/**
|
|
65548
66932
|
* Where the "popover or dialog?" answer lives, for both the components that
|
|
65549
66933
|
* decide it and the content that renders inside one.
|
|
@@ -67294,7 +68678,8 @@ const css$E = /* css */`
|
|
|
67294
68678
|
* wall is shown through the Popover API rather than `showModal()`, which is
|
|
67295
68679
|
* what a sheet docked to the screen's bottom edge over a still-readable map
|
|
67296
68680
|
* needs. What it gives up is what only a modal gets natively: the hardware
|
|
67297
|
-
* back button no longer dismisses it.
|
|
68681
|
+
* back button no longer dismisses it. Not read inside a `<PopupGroup>`,
|
|
68682
|
+
* whose wall it is. See docs/popup_backdrop.md.
|
|
67298
68683
|
* @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="close"]
|
|
67299
68684
|
* - `"close"` closes the dialog on an outside press, `"cancel"` closes it
|
|
67300
68685
|
* as a cancel (what it holds reverts, as on Escape). `"capture"`/`"ignore"`
|
|
@@ -67851,7 +69236,7 @@ const useDialogProps = props => {
|
|
|
67851
69236
|
// before how it is painted (backdropVariant below). It also picks the show
|
|
67852
69237
|
// call for layer="top": a wall is what showModal() is for, and without one
|
|
67853
69238
|
// the dialog goes to the same top layer through the Popover API instead.
|
|
67854
|
-
backdrop = true,
|
|
69239
|
+
backdrop: backdropProp = true,
|
|
67855
69240
|
// How loudly the backdrop says it is there — independent of what it
|
|
67856
69241
|
// *does* (that's pressOutside above). "lift" is the
|
|
67857
69242
|
// opaque wall at one end, "invisible" a wall that is not seen at the
|
|
@@ -67919,9 +69304,20 @@ const useDialogProps = props => {
|
|
|
67919
69304
|
// Where it is placed, and whether there is a wall — the two questions that
|
|
67920
69305
|
// pick the show call (see this file's top comment).
|
|
67921
69306
|
const isTopLayer = layer === "top";
|
|
69307
|
+
// Inside a <PopupGroup> the wall is the group's (see popup_group.jsx): the
|
|
69308
|
+
// dialog takes the wall-less path, never showModal() — a modal dialog makes
|
|
69309
|
+
// everything outside it inert, the group's other popups included.
|
|
69310
|
+
const popupGroup = useContext(PopupGroupContext);
|
|
69311
|
+
useLayoutEffect(() => {
|
|
69312
|
+
if (!popupGroup) {
|
|
69313
|
+
return undefined;
|
|
69314
|
+
}
|
|
69315
|
+
return popupGroup.register(openController);
|
|
69316
|
+
}, [popupGroup, openController]);
|
|
69317
|
+
const backdrop = backdropProp && !popupGroup;
|
|
67922
69318
|
const isModal = isTopLayer && backdrop;
|
|
67923
69319
|
const isTopLayerPopover = isTopLayer && !backdrop;
|
|
67924
|
-
if (!backdrop && pressOutside === "capture") {
|
|
69320
|
+
if (!popupGroup && !backdrop && pressOutside === "capture") {
|
|
67925
69321
|
console.warn(`Dialog: pressOutside="capture" needs a backdrop. Absorbing a press is what a wall does, and backdrop={false} takes it away.`);
|
|
67926
69322
|
}
|
|
67927
69323
|
const ref = props.ref;
|
|
@@ -68158,6 +69554,14 @@ const useDialogProps = props => {
|
|
|
68158
69554
|
if (!dialogEl) {
|
|
68159
69555
|
return undefined;
|
|
68160
69556
|
}
|
|
69557
|
+
// First, so the group already stands where it stands in the top layer
|
|
69558
|
+
// when this dialog shows itself above it.
|
|
69559
|
+
popupGroup?.engage({
|
|
69560
|
+
openController,
|
|
69561
|
+
popupElement: dialogEl,
|
|
69562
|
+
pressOutside,
|
|
69563
|
+
event: e
|
|
69564
|
+
});
|
|
68161
69565
|
|
|
68162
69566
|
// Set by useOpenControllerByProps for the very first open triggered by
|
|
68163
69567
|
// `open`/`defaultOpen` already being truthy at mount, and for an open a
|
|
@@ -68248,7 +69652,8 @@ const useDialogProps = props => {
|
|
|
68248
69652
|
// A modal dialog always has its own ::backdrop; a local one has the
|
|
68249
69653
|
// backdrop element when it renders one.
|
|
68250
69654
|
addCleanup(trapScrollInside(dialogEl, {
|
|
68251
|
-
backdrop
|
|
69655
|
+
// A group's ::backdrop covers the page while this dialog is open.
|
|
69656
|
+
backdrop: popupGroup ? true : isModal || backdropEl
|
|
68252
69657
|
}));
|
|
68253
69658
|
} else if (!isTopLayer) {
|
|
68254
69659
|
// A local dialog is confined to its positioned ancestor, and so is its
|
|
@@ -68479,7 +69884,9 @@ const useDialogProps = props => {
|
|
|
68479
69884
|
// file's top comment), and a local one the caller asked to leave the page
|
|
68480
69885
|
// reachable. Active for the dialog's entire open lifetime, not just
|
|
68481
69886
|
// mid-transition.
|
|
68482
|
-
if ((isModal || !backdrop) &&
|
|
69887
|
+
if ((isModal || !backdrop) &&
|
|
69888
|
+
// The group's wall is up, and the group hears the press itself.
|
|
69889
|
+
!popupGroup && (pressOutside === "close" || pressOutside === "cancel")) {
|
|
68483
69890
|
addCleanup(armOutsidePressClose(dialogEl, {
|
|
68484
69891
|
openController,
|
|
68485
69892
|
pressOutside,
|
|
@@ -68518,6 +69925,11 @@ const useDialogProps = props => {
|
|
|
68518
69925
|
dialogEl.close();
|
|
68519
69926
|
}
|
|
68520
69927
|
};
|
|
69928
|
+
if (!findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
|
|
69929
|
+
// Before its own focus restore: a group engaged inside it is the
|
|
69930
|
+
// topmost modal, and keeps everything else inert until it goes.
|
|
69931
|
+
closePopupGroupsInside(dialogEl, closeEvent);
|
|
69932
|
+
}
|
|
68521
69933
|
if (findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
|
|
68522
69934
|
// Leaving the tree: hidden natively once the commit is over. Its
|
|
68523
69935
|
// removal takes it out of the top layer and hands the focus to
|
|
@@ -68547,9 +69959,11 @@ const useDialogProps = props => {
|
|
|
68547
69959
|
if (backdropEl) {
|
|
68548
69960
|
backdropEl.setAttribute("aria-expanded", "false");
|
|
68549
69961
|
disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent, () => {
|
|
69962
|
+
disarmBackdropHideRef.current = null;
|
|
68550
69963
|
backdropEl.setAttribute("navi-hidden", "");
|
|
68551
69964
|
});
|
|
68552
69965
|
}
|
|
69966
|
+
popupGroup?.release(openController, closeEvent);
|
|
68553
69967
|
restoreFocus(closeEvent);
|
|
68554
69968
|
cleanup();
|
|
68555
69969
|
if (silent) {
|
|
@@ -68565,9 +69979,13 @@ const useDialogProps = props => {
|
|
|
68565
69979
|
};
|
|
68566
69980
|
const onKeyDownShortcuts = createOnKeyDownForShortcuts({
|
|
68567
69981
|
escape: e => {
|
|
68568
|
-
//
|
|
68569
|
-
//
|
|
68570
|
-
|
|
69982
|
+
// Answered at the keydown, modal or not: the innermost popup takes the
|
|
69983
|
+
// key and spends it (requestClose prevents it), so one Escape closes
|
|
69984
|
+
// one popup. A modal's native "cancel" comes only once the keydown has
|
|
69985
|
+
// been dispatched, and the key would reach a popup around this one
|
|
69986
|
+
// first — a picker whose drawing holds this dialog. onCancel below is
|
|
69987
|
+
// left the close requests that are no keydown: the back gesture.
|
|
69988
|
+
if (!openController.opened) {
|
|
68571
69989
|
return null;
|
|
68572
69990
|
}
|
|
68573
69991
|
return {
|
|
@@ -68610,8 +70028,9 @@ const useDialogProps = props => {
|
|
|
68610
70028
|
// there's no gap for the browser to ever paint this plain-div backdrop
|
|
68611
70029
|
// visible before anything has actually opened it — see popover.jsx's
|
|
68612
70030
|
// own identical prop for the full reasoning, and this file's own CSS
|
|
68613
|
-
// for the rule it drives.
|
|
68614
|
-
|
|
70031
|
+
// for the rule it drives. Left up while the press that closed the dialog
|
|
70032
|
+
// has its click still to come, as popover.jsx's own wall is.
|
|
70033
|
+
"navi-hidden": openController.openedInDom || disarmBackdropHideRef.current ? undefined : "",
|
|
68615
70034
|
"styleCSSVars": DIALOG_STYLE_CSS_VARS,
|
|
68616
70035
|
"animationDuration": rest.animationDuration,
|
|
68617
70036
|
"data-press-outside": pressOutside,
|
|
@@ -68738,7 +70157,8 @@ const useDialogProps = props => {
|
|
|
68738
70157
|
handlePressOnOutsideRegion(e, {
|
|
68739
70158
|
popupEl: ref.current,
|
|
68740
70159
|
openController,
|
|
68741
|
-
|
|
70160
|
+
// In a group, the press the group's wall would get.
|
|
70161
|
+
pressOutside: popupGroup ? readPressOutsideInPopupGroup(pressOutside) : pressOutside
|
|
68742
70162
|
});
|
|
68743
70163
|
},
|
|
68744
70164
|
"onKeyDown": e => {
|
|
@@ -68746,9 +70166,9 @@ const useDialogProps = props => {
|
|
|
68746
70166
|
onKeyDownShortcuts(e);
|
|
68747
70167
|
},
|
|
68748
70168
|
"onCancel": e => {
|
|
68749
|
-
// The dialog's own "cancel"
|
|
68750
|
-
//
|
|
68751
|
-
// onKeyDownShortcuts above) fires on the dialog element itself. But a child
|
|
70169
|
+
// The dialog's own "cancel" — a close request that is no keydown (the
|
|
70170
|
+
// back gesture), an Escape being answered at its keydown (see
|
|
70171
|
+
// onKeyDownShortcuts above) — fires on the dialog element itself. But a child
|
|
68752
70172
|
// <input type="file"> also fires a BUBBLING "cancel" when the user
|
|
68753
70173
|
// dismisses the file chooser (per the HTML spec), and it lands here
|
|
68754
70174
|
// too; only the dialog's own cancel means "close".
|
|
@@ -68766,7 +70186,12 @@ const useDialogProps = props => {
|
|
|
68766
70186
|
// prevented it already (see requestClose).
|
|
68767
70187
|
e.preventDefault();
|
|
68768
70188
|
},
|
|
68769
|
-
|
|
70189
|
+
// A popup opened from inside this one is in front of it, not a peer of
|
|
70190
|
+
// it: the group stops here.
|
|
70191
|
+
"children": popupGroup ? jsx(PopupGroupContext.Provider, {
|
|
70192
|
+
value: null,
|
|
70193
|
+
children: children
|
|
70194
|
+
}) : children
|
|
68770
70195
|
});
|
|
68771
70196
|
|
|
68772
70197
|
// Outside-click handling for the wall we render ourselves. A modal's is a
|
|
@@ -69260,13 +70685,6 @@ const css$D = /* css */`
|
|
|
69260
70685
|
* is a `Popover`-level answer only for `"close"`/`"cancel"`; `"capture"` has
|
|
69261
70686
|
* nothing left to absorb without a wall, and `"ignore"` renders no wall anyway.
|
|
69262
70687
|
* 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
70688
|
* @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="ignore"]
|
|
69271
70689
|
* - `"ignore"` (default): no backdrop at all, outside presses pass straight
|
|
69272
70690
|
* through. `"close"` closes the popover on an outside press, `"cancel"`
|
|
@@ -69654,7 +71072,6 @@ const usePopoverProps = props => {
|
|
|
69654
71072
|
// Read at the opening, with the backdrop it would otherwise be worked out
|
|
69655
71073
|
// from (see openEffect).
|
|
69656
71074
|
themeColor,
|
|
69657
|
-
group,
|
|
69658
71075
|
scrollCapture,
|
|
69659
71076
|
focusCapture,
|
|
69660
71077
|
// "auto" (default) → the popover follows its content. "frozen" → measured
|
|
@@ -69716,8 +71133,18 @@ const usePopoverProps = props => {
|
|
|
69716
71133
|
// animation={true} or "auto" always resolves to "sliding" or "scaling"
|
|
69717
71134
|
// (see resolveAutoAnimationKind).
|
|
69718
71135
|
const isAutoAnimation = animation === true || animation === "auto";
|
|
69719
|
-
|
|
69720
|
-
|
|
71136
|
+
|
|
71137
|
+
// Inside a <PopupGroup> the wall is the group's (see popup_group.jsx): this
|
|
71138
|
+
// popover draws none, and the group decides what a press outside does.
|
|
71139
|
+
const popupGroup = useContext(PopupGroupContext);
|
|
71140
|
+
useLayoutEffect(() => {
|
|
71141
|
+
if (!popupGroup) {
|
|
71142
|
+
return undefined;
|
|
71143
|
+
}
|
|
71144
|
+
return popupGroup.register(openController);
|
|
71145
|
+
}, [popupGroup, openController]);
|
|
71146
|
+
const hasBackdrop = pressOutside !== "ignore" && backdrop && !popupGroup;
|
|
71147
|
+
if (!popupGroup && !backdrop && pressOutside === "capture") {
|
|
69721
71148
|
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
71149
|
}
|
|
69723
71150
|
// positionPopover lives in openEffect's closure — created once, when the
|
|
@@ -69781,6 +71208,14 @@ const usePopoverProps = props => {
|
|
|
69781
71208
|
if (!popoverEl) {
|
|
69782
71209
|
return undefined;
|
|
69783
71210
|
}
|
|
71211
|
+
// First, so the group already stands where it stands in the top layer
|
|
71212
|
+
// when this popover reads its anchor and shows itself above it.
|
|
71213
|
+
popupGroup?.engage({
|
|
71214
|
+
openController,
|
|
71215
|
+
popupElement: popoverEl,
|
|
71216
|
+
pressOutside,
|
|
71217
|
+
event: e
|
|
71218
|
+
});
|
|
69784
71219
|
|
|
69785
71220
|
// Set by useOpenControllerByProps for the very first open triggered by
|
|
69786
71221
|
// `open`/`defaultOpen` already being truthy at mount — there's nothing
|
|
@@ -70068,8 +71503,9 @@ const usePopoverProps = props => {
|
|
|
70068
71503
|
|
|
70069
71504
|
// With no backdrop of its own to catch a press, the popover hears an
|
|
70070
71505
|
// 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
|
-
|
|
71506
|
+
// the press goes on to whatever it landed on (armOutsidePressClose). Not
|
|
71507
|
+
// inside a group: the group's wall is up, and the group hears the press.
|
|
71508
|
+
if (!hasBackdrop && !popupGroup && (pressOutside === "close" || pressOutside === "cancel")) {
|
|
70073
71509
|
addCleanup(armOutsidePressClose(popoverEl, {
|
|
70074
71510
|
openController,
|
|
70075
71511
|
pressOutside
|
|
@@ -70077,7 +71513,8 @@ const usePopoverProps = props => {
|
|
|
70077
71513
|
}
|
|
70078
71514
|
if (scrollCapture) {
|
|
70079
71515
|
addCleanup(trapScrollInside(popoverEl, {
|
|
70080
|
-
backdrop
|
|
71516
|
+
// A group's ::backdrop covers the page while this popover is open.
|
|
71517
|
+
backdrop: popupGroup ? true : backdropEl
|
|
70081
71518
|
}));
|
|
70082
71519
|
}
|
|
70083
71520
|
if (focusCapture) {
|
|
@@ -70297,6 +71734,11 @@ const usePopoverProps = props => {
|
|
|
70297
71734
|
// comment (openEffect above) for why hidePopover() alone isn't
|
|
70298
71735
|
// reliably sufficient once a consumer's own CSS also sets display.
|
|
70299
71736
|
popoverEl.setAttribute("navi-hidden", "");
|
|
71737
|
+
if (!findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
|
|
71738
|
+
// Before its own focus restore: a group engaged inside it is the
|
|
71739
|
+
// topmost modal, and keeps everything else inert until it goes.
|
|
71740
|
+
closePopupGroupsInside(popoverEl, closeEvent);
|
|
71741
|
+
}
|
|
70300
71742
|
if (isTopLayer) {
|
|
70301
71743
|
// Left to its removal when the popover leaves the tree: that hides it
|
|
70302
71744
|
// and hands the focus to nobody, where hidePopover() hands the focus
|
|
@@ -70323,23 +71765,17 @@ const usePopoverProps = props => {
|
|
|
70323
71765
|
}
|
|
70324
71766
|
if (backdropEl) {
|
|
70325
71767
|
backdropEl.setAttribute("aria-expanded", "false");
|
|
70326
|
-
disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent,
|
|
71768
|
+
disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent, () => {
|
|
71769
|
+
disarmBackdropHideRef.current = null;
|
|
70327
71770
|
// Set regardless of isTopLayer — see openEffect's own identical
|
|
70328
71771
|
// comment for why hidePopover() alone isn't reliably sufficient.
|
|
70329
71772
|
backdropEl.setAttribute("navi-hidden", "");
|
|
70330
71773
|
if (isTopLayer) {
|
|
70331
71774
|
backdropEl.hidePopover();
|
|
70332
71775
|
}
|
|
70333
|
-
if (group && clickEvent) {
|
|
70334
|
-
handWallClickToPeer(clickEvent, {
|
|
70335
|
-
wallEl: backdropEl,
|
|
70336
|
-
popupEl: popoverEl,
|
|
70337
|
-
anchorEl: anchorElement,
|
|
70338
|
-
group
|
|
70339
|
-
});
|
|
70340
|
-
}
|
|
70341
71776
|
});
|
|
70342
71777
|
}
|
|
71778
|
+
popupGroup?.release(openController, closeEvent);
|
|
70343
71779
|
restoreFocus?.(closeEvent);
|
|
70344
71780
|
|
|
70345
71781
|
// pickPositionRelativeTo (visible_rect.js) writes data-position-y/x-
|
|
@@ -70418,8 +71854,11 @@ const usePopoverProps = props => {
|
|
|
70418
71854
|
// Read fresh on every render (not frozen at mount), so it stays
|
|
70419
71855
|
// correct even across a re-render that happens to occur while open —
|
|
70420
71856
|
// see contentProps' own identical prop just below for the full
|
|
70421
|
-
// reasoning (kept once, not repeated here).
|
|
70422
|
-
|
|
71857
|
+
// reasoning (kept once, not repeated here). The wall outlives its popup
|
|
71858
|
+
// by the press that closed it (armPointerDownOutsideClose): a render
|
|
71859
|
+
// before that press's click must leave it up — a popup's owner commonly
|
|
71860
|
+
// renders on its close, in a microtask the release comes after.
|
|
71861
|
+
"navi-hidden": openController.openedInDom || disarmBackdropHideRef.current ? undefined : "",
|
|
70423
71862
|
"styleCSSVars": POPUP_STYLE_CSS_VARS,
|
|
70424
71863
|
"animationDuration": rest.animationDuration,
|
|
70425
71864
|
"data-press-outside": pressOutside,
|
|
@@ -70517,14 +71956,20 @@ const usePopoverProps = props => {
|
|
|
70517
71956
|
handlePressOnOutsideRegion(e, {
|
|
70518
71957
|
popupEl: ref.current,
|
|
70519
71958
|
openController,
|
|
70520
|
-
|
|
71959
|
+
// In a group, the press the group's wall would get.
|
|
71960
|
+
pressOutside: popupGroup ? readPressOutsideInPopupGroup(pressOutside) : pressOutside
|
|
70521
71961
|
});
|
|
70522
71962
|
},
|
|
70523
71963
|
"onKeyDown": e => {
|
|
70524
71964
|
onKeyDown?.(e);
|
|
70525
71965
|
onKeyDownShortcuts(e);
|
|
70526
71966
|
},
|
|
70527
|
-
|
|
71967
|
+
// A popup opened from inside this one is in front of it, not a peer of
|
|
71968
|
+
// it: the group stops here.
|
|
71969
|
+
"children": popupGroup ? jsx(PopupGroupContext.Provider, {
|
|
71970
|
+
value: null,
|
|
71971
|
+
children: children
|
|
71972
|
+
}) : children
|
|
70528
71973
|
// onnavi_request_open/onnavi_request_close/onnavi_open_press: for the
|
|
70529
71974
|
// uncontrolled case,
|
|
70530
71975
|
// already arrive here as plain props via ...rest (wired by
|
|
@@ -70774,10 +72219,6 @@ const Popup = props => {
|
|
|
70774
72219
|
scrollCapture,
|
|
70775
72220
|
positionAreaFixed,
|
|
70776
72221
|
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
72222
|
// Dialog-only, destructured out for the same reason: forwarded below only
|
|
70782
72223
|
// in the dialog branch, so they never reach the popover element.
|
|
70783
72224
|
dockedOnSmallTouchScreen,
|
|
@@ -70827,7 +72268,6 @@ const Popup = props => {
|
|
|
70827
72268
|
positionAreaWhenAnchorIsInvalid: positionAreaWhenAnchorIsInvalid,
|
|
70828
72269
|
scrollCapture: scrollCapture === "popover" || scrollCapture,
|
|
70829
72270
|
positionAreaFixed: positionAreaFixed,
|
|
70830
|
-
group: group,
|
|
70831
72271
|
className: withPropsClassName("navi_popup", className),
|
|
70832
72272
|
expand: expand,
|
|
70833
72273
|
expandX: expandX,
|
|
@@ -71357,6 +72797,10 @@ const PickerCustom = props => {
|
|
|
71357
72797
|
}
|
|
71358
72798
|
};
|
|
71359
72799
|
});
|
|
72800
|
+
// Whoever hears the Escape — this picker's trigger, its popup, the group
|
|
72801
|
+
// it stands in, its bubble — asks for a cancel; the controller reads it
|
|
72802
|
+
// with this (see escapeEffect in open_controller.js).
|
|
72803
|
+
openController.escapeEffect = escapeEffect;
|
|
71360
72804
|
// scroll <button> of the picker into view when opening it
|
|
71361
72805
|
// -> would be overriden by dialog.jsx or popover.jsx
|
|
71362
72806
|
// so ideally openEffect should be either protective or a pubSub to allow multiple callbacks
|
|
@@ -71582,13 +73026,13 @@ const PickerCustom = props => {
|
|
|
71582
73026
|
if (!openController.opened) {
|
|
71583
73027
|
return null;
|
|
71584
73028
|
}
|
|
71585
|
-
const isCancel = escapeEffect === "cancel";
|
|
71586
73029
|
return {
|
|
71587
|
-
name:
|
|
73030
|
+
name: "escape_to_cancel",
|
|
71588
73031
|
intent: "read",
|
|
71589
73032
|
allowed: () => {
|
|
73033
|
+
// Read with escapeEffect by the controller (see above).
|
|
71590
73034
|
requestClose(e, {
|
|
71591
|
-
isCancel
|
|
73035
|
+
isCancel: true
|
|
71592
73036
|
});
|
|
71593
73037
|
e.preventDefault(); // prevent browser from closing the dialog (if any)
|
|
71594
73038
|
}
|
|
@@ -71854,18 +73298,13 @@ const PickerContentInsidePopup = props => {
|
|
|
71854
73298
|
calloutIcon,
|
|
71855
73299
|
calloutCloseButton,
|
|
71856
73300
|
calloutCloseByScroll,
|
|
71857
|
-
popupGroup,
|
|
71858
73301
|
...rest
|
|
71859
73302
|
} = props;
|
|
71860
73303
|
const isPopover = mode === "popover";
|
|
71861
73304
|
const isCallout = mode === "callout";
|
|
71862
73305
|
return jsx(Next, {
|
|
71863
73306
|
"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,
|
|
73307
|
+
"navi-popover-mode": isPopover ? popoverMode : undefined,
|
|
71869
73308
|
...rest,
|
|
71870
73309
|
// On popupProps already (see the picker's popup assembly); they mean
|
|
71871
73310
|
// nothing to the picker element.
|
|
@@ -71906,7 +73345,7 @@ const PickerContentInsidePopup = props => {
|
|
|
71906
73345
|
// nothing, so the page looks as pressable as it did before.
|
|
71907
73346
|
,
|
|
71908
73347
|
backdrop: backdrop,
|
|
71909
|
-
|
|
73348
|
+
pressOutside: pressOutside,
|
|
71910
73349
|
children: jsx(PopupModeContext.Provider, {
|
|
71911
73350
|
value: mode,
|
|
71912
73351
|
children: children
|
|
@@ -71943,7 +73382,6 @@ const PickerContentInsidePopup = props => {
|
|
|
71943
73382
|
backdropVariant: backdropVariant,
|
|
71944
73383
|
backdropColor: backdropColor,
|
|
71945
73384
|
backdropFilter: backdropFilter,
|
|
71946
|
-
group: popupGroup,
|
|
71947
73385
|
focusCapture: isPopover ? focusCapture : undefined,
|
|
71948
73386
|
expand: isPopover ? undefined : dialogExpand,
|
|
71949
73387
|
expandX: isPopover ? undefined : dialogExpandX,
|
|
@@ -72004,7 +73442,7 @@ const PickerCalloutPopup = ({
|
|
|
72004
73442
|
closeButton,
|
|
72005
73443
|
closeByScroll,
|
|
72006
73444
|
backdrop,
|
|
72007
|
-
|
|
73445
|
+
pressOutside,
|
|
72008
73446
|
onnavi_request_open,
|
|
72009
73447
|
onnavi_request_close,
|
|
72010
73448
|
onnavi_request_confirm,
|
|
@@ -72021,6 +73459,16 @@ const PickerCalloutPopup = ({
|
|
|
72021
73459
|
anchor
|
|
72022
73460
|
});
|
|
72023
73461
|
const children = contentMounted ? childrenProp : null;
|
|
73462
|
+
// Inside a <PopupGroup> the wall is the group's (see popup_group.jsx): it
|
|
73463
|
+
// decides what a press outside the callout does, and the callout spends and
|
|
73464
|
+
// closes on none of its own.
|
|
73465
|
+
const popupGroup = useContext(PopupGroupContext);
|
|
73466
|
+
useLayoutEffect(() => {
|
|
73467
|
+
if (!popupGroup) {
|
|
73468
|
+
return undefined;
|
|
73469
|
+
}
|
|
73470
|
+
return popupGroup.register(openController);
|
|
73471
|
+
}, [popupGroup, openController]);
|
|
72024
73472
|
// Reassigned on every render, like Popover's own, so it closes over the
|
|
72025
73473
|
// latest props.
|
|
72026
73474
|
openController.getElement = () => pickerRef.current;
|
|
@@ -72035,6 +73483,23 @@ const PickerCalloutPopup = ({
|
|
|
72035
73483
|
// picker's own input — which is where the data-callout-* attributes a
|
|
72036
73484
|
// caller puts on the picker land, and where the callout reads them.
|
|
72037
73485
|
const anchorElement = anchor === pickerRef ? undefined : anchor && "current" in anchor ? anchor.current : anchor;
|
|
73486
|
+
// First, so the group already stands where it stands in the top layer
|
|
73487
|
+
// when the callout reads its anchor and shows itself above it.
|
|
73488
|
+
popupGroup?.engage({
|
|
73489
|
+
openController,
|
|
73490
|
+
// Drawn by the manager below, once the group stands.
|
|
73491
|
+
get popupElement() {
|
|
73492
|
+
return calloutManager.callout.element;
|
|
73493
|
+
},
|
|
73494
|
+
// What a data-navi-popup-inside names this popup by (aria-controls
|
|
73495
|
+
// names it too): the dock, not the callout drawn for it.
|
|
73496
|
+
popupId: id,
|
|
73497
|
+
pressOutside,
|
|
73498
|
+
event: openEvent,
|
|
73499
|
+
// A callout never holds the page still: a scroll is its own
|
|
73500
|
+
// closeByScroll's to answer.
|
|
73501
|
+
pageScrolls: true
|
|
73502
|
+
});
|
|
72038
73503
|
calloutManager.addOpenToken(PICKER_CALLOUT_CONTENT_TOKEN, {
|
|
72039
73504
|
message: host,
|
|
72040
73505
|
// The popup a `popupTestId` names is this callout: it is the surface the
|
|
@@ -72046,8 +73511,11 @@ const PickerCalloutPopup = ({
|
|
|
72046
73511
|
icon,
|
|
72047
73512
|
closeButton,
|
|
72048
73513
|
closeByScroll,
|
|
72049
|
-
backdrop,
|
|
72050
|
-
|
|
73514
|
+
backdrop: popupGroup ? false : backdrop,
|
|
73515
|
+
closeByPressOutside: popupGroup ? false : undefined,
|
|
73516
|
+
// Inside the group whatever it points at: what is not a descendant of
|
|
73517
|
+
// an engaged group is inert, a bubble drawn there included.
|
|
73518
|
+
containerElement: popupGroup ? pickerEl : undefined,
|
|
72051
73519
|
anchorElement,
|
|
72052
73520
|
// The request, chained to the press that made it: the callout reads the
|
|
72053
73521
|
// mousedown off it to wait for the release before listening for a click
|
|
@@ -72056,10 +73524,15 @@ const PickerCalloutPopup = ({
|
|
|
72056
73524
|
// Not skipped: the callout moves the focus into the picker when it is
|
|
72057
73525
|
// elsewhere, which is what lets Escape find the callout right away.
|
|
72058
73526
|
skipFocus: false,
|
|
73527
|
+
// An Escape the callout answered itself (the focus in the bubble) is
|
|
73528
|
+
// the picker's Escape (see escapeEffect in open_controller.js).
|
|
72059
73529
|
onClose: ({
|
|
72060
|
-
event
|
|
73530
|
+
event,
|
|
73531
|
+
reason
|
|
72061
73532
|
}) => {
|
|
72062
|
-
openController.close(event
|
|
73533
|
+
openController.close(event, {
|
|
73534
|
+
isCancel: reason === "escape_key"
|
|
73535
|
+
});
|
|
72063
73536
|
}
|
|
72064
73537
|
});
|
|
72065
73538
|
// A --navi-confirm said inside the callout is aimed at the callout (its
|
|
@@ -72077,9 +73550,15 @@ const PickerCalloutPopup = ({
|
|
|
72077
73550
|
dock.setAttribute("aria-expanded", "true");
|
|
72078
73551
|
return closeEvent => {
|
|
72079
73552
|
calloutElement.removeEventListener("navi_request_confirm", forwardConfirm);
|
|
73553
|
+
if (!findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
|
|
73554
|
+
// Before the content goes back to the dock: a group engaged in there
|
|
73555
|
+
// would leave the top layer with it, its arbiter still listening.
|
|
73556
|
+
closePopupGroupsInside(calloutElement, closeEvent);
|
|
73557
|
+
}
|
|
72080
73558
|
calloutManager.removeOpenToken(PICKER_CALLOUT_CONTENT_TOKEN, closeEvent);
|
|
72081
73559
|
dock.appendChild(host);
|
|
72082
73560
|
dock.setAttribute("aria-expanded", "false");
|
|
73561
|
+
popupGroup?.release(openController, closeEvent);
|
|
72083
73562
|
};
|
|
72084
73563
|
};
|
|
72085
73564
|
return (
|
|
@@ -72107,7 +73586,10 @@ const PickerCalloutPopup = ({
|
|
|
72107
73586
|
children: jsx("div", {
|
|
72108
73587
|
ref: hostRef,
|
|
72109
73588
|
"data-picker-content": "",
|
|
72110
|
-
children:
|
|
73589
|
+
children: popupGroup ? jsx(PopupGroupContext.Provider, {
|
|
73590
|
+
value: null,
|
|
73591
|
+
children: children
|
|
73592
|
+
}) : children
|
|
72111
73593
|
})
|
|
72112
73594
|
})
|
|
72113
73595
|
);
|
|
@@ -73631,7 +75113,11 @@ const css$y = /* css */`@layer navi {
|
|
|
73631
75113
|
}
|
|
73632
75114
|
}
|
|
73633
75115
|
|
|
73634
|
-
&
|
|
75116
|
+
& [popover] {
|
|
75117
|
+
pointer-events: auto;
|
|
75118
|
+
}
|
|
75119
|
+
|
|
75120
|
+
& dialog:not([navi-popup-group]:not(:modal)) {
|
|
73635
75121
|
pointer-events: auto;
|
|
73636
75122
|
}
|
|
73637
75123
|
}
|
|
@@ -74429,7 +75915,7 @@ const css$x = /* css */`@property --list-group-label-height {
|
|
|
74429
75915
|
background: var(--list-background-color);
|
|
74430
75916
|
}
|
|
74431
75917
|
|
|
74432
|
-
:where([popover], dialog) > .navi_list_container, .navi_list_container[popover] {
|
|
75918
|
+
:where([popover], dialog:not([navi-popup-group])) > .navi_list_container, .navi_list_container[popover] {
|
|
74433
75919
|
--list-border-width-default: 0px;
|
|
74434
75920
|
}
|
|
74435
75921
|
|
|
@@ -95830,8 +97316,9 @@ const css = /* css */`.navi_side_panel {
|
|
|
95830
97316
|
* interaction only makes sense paired with not letting focus silently
|
|
95831
97317
|
* leave the panel first. A box of the page whose press must not close the
|
|
95832
97318
|
* 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}
|
|
97319
|
+
* `data-navi-popup-inside={id}`, read when the panel has no wall of its own
|
|
97320
|
+
* (`backdrop={false}`, or inside a `<PopupGroup>`) — see
|
|
97321
|
+
* docs/popup_backdrop.md.
|
|
95835
97322
|
* @param {boolean} [props.closeByDrag=true] - Pushing the panel back
|
|
95836
97323
|
* towards the edge it is docked to closes it: the panel follows the
|
|
95837
97324
|
* pointer and finishes leaving (or comes back to rest) when it is
|
|
@@ -96277,5 +97764,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
96277
97764
|
})
|
|
96278
97765
|
});
|
|
96279
97766
|
|
|
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 };
|
|
97767
|
+
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
97768
|
//# sourceMappingURL=jsenv_navi.js.map
|