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