@jsenv/navi 0.29.415 → 0.29.417
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/dist/dev/jsenv_navi.js +1524 -1109
- package/dist/dev/jsenv_navi.js.map +18 -16
- package/dist/jsenv_navi.js +955 -554
- package/dist/jsenv_navi.js.map +18 -16
- package/docs/AI_INSTRUCTIONS.md +8 -4
- package/docs/mobile_touch.md +35 -0
- package/docs/resource.md +20 -3
- package/docs/route_transitions.md +29 -7
- 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, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet,
|
|
6
|
+
import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, ELEMENT_SIZE_CHANGE, getScrollContainer, scrollIntoViewScoped, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, trapScrollInside, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange, getVirtualKeyboardOverlayHeight } from "@jsenv/dom";
|
|
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, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
|
|
@@ -15245,6 +15245,572 @@ const useOpenControllerByProps = (props, name) => {
|
|
|
15245
15245
|
return openController;
|
|
15246
15246
|
};
|
|
15247
15247
|
|
|
15248
|
+
/**
|
|
15249
|
+
* Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
|
|
15250
|
+
* (non-top-layer) renderers — operate on a plain DOM element, no knowledge
|
|
15251
|
+
* of which of the two owns it.
|
|
15252
|
+
*/
|
|
15253
|
+
|
|
15254
|
+
|
|
15255
|
+
/**
|
|
15256
|
+
* Whether a visibleRectEffect delivery is one that can have taken height away
|
|
15257
|
+
* from a popup, and so pushed whatever holds focus out of sight:
|
|
15258
|
+
* - "resize": the window/visual viewport settled — which is also how the
|
|
15259
|
+
* on-screen keyboard arrives, overlay or not (window_size.js);
|
|
15260
|
+
* - ELEMENT_SIZE_CHANGE: the popup's own box measured different;
|
|
15261
|
+
* - "focusin": the focus-settled re-measure, for the room that changes with
|
|
15262
|
+
* nothing announcing it (subscribeFocusSettled in window_size.js).
|
|
15263
|
+
*
|
|
15264
|
+
* Everything else is a scroll of one kind or another, where nothing got
|
|
15265
|
+
* smaller and scrolling the focused element back would fight the very gesture
|
|
15266
|
+
* that fired it.
|
|
15267
|
+
*/
|
|
15268
|
+
const mayHaveHiddenFocus = (event) => {
|
|
15269
|
+
const type = event?.type;
|
|
15270
|
+
return (
|
|
15271
|
+
type === "resize" || type === ELEMENT_SIZE_CHANGE || type === "focusin"
|
|
15272
|
+
);
|
|
15273
|
+
};
|
|
15274
|
+
|
|
15275
|
+
const scrollportHeightMap = new WeakMap();
|
|
15276
|
+
/**
|
|
15277
|
+
* Scrolls whatever holds focus inside `popupEl` back into view, if the popup
|
|
15278
|
+
* getting shorter has pushed it out.
|
|
15279
|
+
*
|
|
15280
|
+
* The case this exists for: a field low in the scrolling body of a popup that
|
|
15281
|
+
* also has a footer (box.jsx — with a body, the body is the only thing that
|
|
15282
|
+
* scrolls and the footer is a sibling sitting right under it). Focusing the
|
|
15283
|
+
* field makes the browser scroll it into view, which it does against the
|
|
15284
|
+
* popup's height AT THAT MOMENT; the on-screen keyboard then opens and takes
|
|
15285
|
+
* that height away. The body shrinks, its scrollTop does not move, so the
|
|
15286
|
+
* content slides down relative to the shorter scrollport and the field ends up
|
|
15287
|
+
* past its bottom edge — visually, swallowed by the footer. The browser does
|
|
15288
|
+
* not redo a scroll-into-view it already answered, so this does.
|
|
15289
|
+
*
|
|
15290
|
+
* Only what the shrink itself hid, though: with the keyboard up the user reads
|
|
15291
|
+
* the rest of the popup by scrolling the field away — to reach the submit under
|
|
15292
|
+
* it, typically — and the room keeps changing while they do (a keyboard settling
|
|
15293
|
+
* in two steps, a suggestion strip, a browser bar). Answering each of those by
|
|
15294
|
+
* scrolling the field back takes the popup away from wherever they had just
|
|
15295
|
+
* scrolled it, over and over: what they were reading cannot be reached at all
|
|
15296
|
+
* without blurring the field first, and the popup reads as unscrollable. So the
|
|
15297
|
+
* field is brought back only when it was in view before the room shrank, which
|
|
15298
|
+
* one remembered number answers: a resize moves neither scrollTop nor the
|
|
15299
|
+
* element's offset inside the scrolled content, so measuring against the height
|
|
15300
|
+
* the scrollport HAD is measuring the state before the change.
|
|
15301
|
+
*
|
|
15302
|
+
* Scoped to the field's own scroll container (never the page): a popup traps
|
|
15303
|
+
* scrolling precisely so the document underneath cannot move, and a plain
|
|
15304
|
+
* scrollIntoView walks past a container whose scrollbar isn't visible — see
|
|
15305
|
+
* scrollIntoViewScoped's own doc.
|
|
15306
|
+
*
|
|
15307
|
+
* "nearest": the smallest scroll that makes it visible, and none at all when it
|
|
15308
|
+
* already is. Where it lands is the container's own business — a navi scroller
|
|
15309
|
+
* keeps a band free at its edges so a field never comes back glued to one (see
|
|
15310
|
+
* scroll-padding in box.jsx).
|
|
15311
|
+
*/
|
|
15312
|
+
const keepFocusedElementVisible = (popupEl) => {
|
|
15313
|
+
const { activeElement } = document;
|
|
15314
|
+
if (!activeElement || activeElement === popupEl) {
|
|
15315
|
+
return;
|
|
15316
|
+
}
|
|
15317
|
+
if (!popupEl.contains(activeElement)) {
|
|
15318
|
+
return;
|
|
15319
|
+
}
|
|
15320
|
+
const scrollContainer = getScrollContainer(activeElement);
|
|
15321
|
+
if (!scrollContainer || !popupEl.contains(scrollContainer)) {
|
|
15322
|
+
// What scrolls the field is outside the popup, which means the page: a
|
|
15323
|
+
// popup holds it still on purpose (trapScrollInside), so there is nothing
|
|
15324
|
+
// here to scroll back.
|
|
15325
|
+
return;
|
|
15326
|
+
}
|
|
15327
|
+
const scrollportHeight = scrollContainer.clientHeight;
|
|
15328
|
+
const scrollportHeightBefore = scrollportHeightMap.get(scrollContainer);
|
|
15329
|
+
scrollportHeightMap.set(scrollContainer, scrollportHeight);
|
|
15330
|
+
if (scrollportHeightBefore !== undefined) {
|
|
15331
|
+
if (scrollportHeight >= scrollportHeightBefore) {
|
|
15332
|
+
// Nothing was taken away, so nothing was hidden by this.
|
|
15333
|
+
return;
|
|
15334
|
+
}
|
|
15335
|
+
if (
|
|
15336
|
+
!isVisibleInScrollport(
|
|
15337
|
+
activeElement,
|
|
15338
|
+
scrollContainer,
|
|
15339
|
+
scrollportHeightBefore,
|
|
15340
|
+
)
|
|
15341
|
+
) {
|
|
15342
|
+
return;
|
|
15343
|
+
}
|
|
15344
|
+
}
|
|
15345
|
+
scrollIntoViewScoped(activeElement, {
|
|
15346
|
+
container: scrollContainer,
|
|
15347
|
+
block: "nearest",
|
|
15348
|
+
});
|
|
15349
|
+
};
|
|
15350
|
+
|
|
15351
|
+
// Whether any part of `el` was showing in `container` back when its scrollport
|
|
15352
|
+
// was `scrollportHeight` tall. Both boxes are read now: the container may have
|
|
15353
|
+
// moved as well as shrunk (a centered dialog re-centers itself), and the
|
|
15354
|
+
// difference between the two tops is what that move leaves alone.
|
|
15355
|
+
const isVisibleInScrollport = (el, container, scrollportHeight) => {
|
|
15356
|
+
const elRect = el.getBoundingClientRect();
|
|
15357
|
+
const containerRect = container.getBoundingClientRect();
|
|
15358
|
+
const elTop = elRect.top - containerRect.top + container.scrollTop;
|
|
15359
|
+
const elBottom = elTop + elRect.height;
|
|
15360
|
+
const scrollportTop = container.scrollTop;
|
|
15361
|
+
const scrollportBottom = scrollportTop + scrollportHeight;
|
|
15362
|
+
return elBottom > scrollportTop && elTop < scrollportBottom;
|
|
15363
|
+
};
|
|
15364
|
+
|
|
15365
|
+
/**
|
|
15366
|
+
* Calls `onSettled` once `el`'s current CSS transition is over — via
|
|
15367
|
+
* `transitionend`, with a safety `setTimeout` fallback matching the longest
|
|
15368
|
+
* `transition-duration`, in case nothing actually transitions or an event is
|
|
15369
|
+
* missed.
|
|
15370
|
+
*
|
|
15371
|
+
* Returns a "cancel" function, so a caller whose instance has been superseded
|
|
15372
|
+
* (a fresh open/close about to set its own state) can keep this stale one from
|
|
15373
|
+
* firing later. Cancelling only stops `onSettled`: undoing whatever the caller
|
|
15374
|
+
* did up front is that fresh call's business, not this one's.
|
|
15375
|
+
*/
|
|
15376
|
+
const whenTransitionSettles = (el, onSettled) => {
|
|
15377
|
+
let settled = false;
|
|
15378
|
+
const onTransitionEnd = (transitionEvent) => {
|
|
15379
|
+
if (transitionEvent.target === el) {
|
|
15380
|
+
finish();
|
|
15381
|
+
}
|
|
15382
|
+
};
|
|
15383
|
+
const stopWatching = () => {
|
|
15384
|
+
settled = true;
|
|
15385
|
+
el.removeEventListener("transitionend", onTransitionEnd);
|
|
15386
|
+
clearTimeout(safetyTimeoutId);
|
|
15387
|
+
};
|
|
15388
|
+
const finish = () => {
|
|
15389
|
+
if (settled) {
|
|
15390
|
+
return;
|
|
15391
|
+
}
|
|
15392
|
+
stopWatching();
|
|
15393
|
+
onSettled();
|
|
15394
|
+
};
|
|
15395
|
+
el.addEventListener("transitionend", onTransitionEnd);
|
|
15396
|
+
const durationsInSeconds = getComputedStyle(el)
|
|
15397
|
+
.transitionDuration.split(",")
|
|
15398
|
+
.map((value) => parseFloat(value) || 0);
|
|
15399
|
+
const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
|
|
15400
|
+
const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
|
|
15401
|
+
return () => {
|
|
15402
|
+
if (settled) {
|
|
15403
|
+
return;
|
|
15404
|
+
}
|
|
15405
|
+
stopWatching();
|
|
15406
|
+
};
|
|
15407
|
+
};
|
|
15408
|
+
|
|
15409
|
+
/**
|
|
15410
|
+
* Drops the document's text selection when it lives inside `el`, leaving a
|
|
15411
|
+
* selection made elsewhere on the page alone.
|
|
15412
|
+
*
|
|
15413
|
+
* A popup being closed takes its content with it, and a selection is a claim
|
|
15414
|
+
* on content the user can still act on: kept, it would outlive the surface it
|
|
15415
|
+
* was made on — painted on the box for the length of its exit transition, with
|
|
15416
|
+
* the handles and the copy toolbar a phone draws from the live selection
|
|
15417
|
+
* hanging over something that is going away — and reappear with the box on
|
|
15418
|
+
* the next opening. Dropping the selection is the only way to remove that
|
|
15419
|
+
* chrome: a `user-select: none` on the closing box hides the highlight in some
|
|
15420
|
+
* browsers only, and the handles are the browser's own, drawn from the
|
|
15421
|
+
* selection object rather than from any style.
|
|
15422
|
+
*/
|
|
15423
|
+
const clearTextSelectionInside = (el) => {
|
|
15424
|
+
const selection = window.getSelection();
|
|
15425
|
+
if (!selection || selection.rangeCount === 0) {
|
|
15426
|
+
return;
|
|
15427
|
+
}
|
|
15428
|
+
const range = selection.getRangeAt(0);
|
|
15429
|
+
if (!el.contains(range.commonAncestorContainer)) {
|
|
15430
|
+
return;
|
|
15431
|
+
}
|
|
15432
|
+
selection.removeAllRanges();
|
|
15433
|
+
};
|
|
15434
|
+
|
|
15435
|
+
/**
|
|
15436
|
+
* Disables pointer-events on `el` until its current CSS transition settles —
|
|
15437
|
+
* avoids the cursor changing/something becoming clickable while the popup is
|
|
15438
|
+
* still visually moving into or out of place.
|
|
15439
|
+
*
|
|
15440
|
+
* Returns whenTransitionSettles' own "cancel" function: it doesn't restore
|
|
15441
|
+
* pointer-events, since a fresh call for the next open/close is about to set
|
|
15442
|
+
* its own state.
|
|
15443
|
+
*/
|
|
15444
|
+
const suppressPointerEventsDuringTransition = (el) => {
|
|
15445
|
+
el.style.pointerEvents = "none";
|
|
15446
|
+
return whenTransitionSettles(el, () => {
|
|
15447
|
+
el.style.pointerEvents = "";
|
|
15448
|
+
});
|
|
15449
|
+
};
|
|
15450
|
+
|
|
15451
|
+
/**
|
|
15452
|
+
* Hides the backdrop, deferring until the browser's matching "click" fires
|
|
15453
|
+
* when `closeEvent` was triggered by a mousedown (see popover.jsx's top
|
|
15454
|
+
* comment for why) — same capture-phase-on-document pattern as
|
|
15455
|
+
* armSuppressNextOpenRequest in open_controller.js, which a plain timeout
|
|
15456
|
+
* can't safely replace: mouseup (and the click that follows it) can land an
|
|
15457
|
+
* arbitrarily long time after mousedown (the user is still holding the
|
|
15458
|
+
* button down), so a short timeout can fire first and hide the backdrop
|
|
15459
|
+
* before its own click ever arrives. A capture-phase listener on document
|
|
15460
|
+
* fires for every click regardless of what any bubble-phase handler does
|
|
15461
|
+
* downstream, so no fallback timer is needed.
|
|
15462
|
+
*
|
|
15463
|
+
* `hide` is the caller's own way to actually hide the backdrop
|
|
15464
|
+
* (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
|
|
15465
|
+
* for a plain div) — this helper only owns the mousedown/click timing.
|
|
15466
|
+
*
|
|
15467
|
+
* Returns a disarm function (or undefined if hidden immediately), so a
|
|
15468
|
+
* fresh open can cancel a pending hide it's about to make redundant.
|
|
15469
|
+
*/
|
|
15470
|
+
const armPointerDownOutsideClose = (closeEvent, hide) => {
|
|
15471
|
+
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
15472
|
+
if (!mousedownEvent) {
|
|
15473
|
+
hide();
|
|
15474
|
+
return undefined;
|
|
15475
|
+
}
|
|
15476
|
+
// `hide` is handed the click, for a wall with peers to re-aim it (see
|
|
15477
|
+
// handWallClickToPeer).
|
|
15478
|
+
const onClick = (clickEvent) => {
|
|
15479
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
15480
|
+
hide(clickEvent);
|
|
15481
|
+
};
|
|
15482
|
+
document.addEventListener("click", onClick, { capture: true });
|
|
15483
|
+
return () => {
|
|
15484
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
15485
|
+
};
|
|
15486
|
+
};
|
|
15487
|
+
|
|
15488
|
+
/**
|
|
15489
|
+
* Maps a positionArea y/x pair to a concrete `navi-animation` value (a
|
|
15490
|
+
* `prefix` plus a direction word), or `null` if both axes overlap the anchor
|
|
15491
|
+
* (no direction at all — that's `resolvedAnimationKind === "scaling"`
|
|
15492
|
+
* territory instead, see resolveAutoAnimationKind below).
|
|
15493
|
+
*
|
|
15494
|
+
* `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
|
|
15495
|
+
* when docked) keeps the word as the compass direction the popup comes
|
|
15496
|
+
* from: placed "top" (a point/corner), it slides in from the top.
|
|
15497
|
+
* `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
|
|
15498
|
+
* direction instead, the opposite compass point: placed "top" of the
|
|
15499
|
+
* anchor, it moves/grows up, away from the anchor (which sits below it).
|
|
15500
|
+
*
|
|
15501
|
+
* "inset-*"/"center" contribute no direction on their axis either way.
|
|
15502
|
+
*/
|
|
15503
|
+
const resolveDirectionValue = (y, x, { prefix }) => {
|
|
15504
|
+
const yWord =
|
|
15505
|
+
y === "top"
|
|
15506
|
+
? prefix === "expand"
|
|
15507
|
+
? "up"
|
|
15508
|
+
: "top"
|
|
15509
|
+
: y === "bottom"
|
|
15510
|
+
? prefix === "expand"
|
|
15511
|
+
? "down"
|
|
15512
|
+
: "bottom"
|
|
15513
|
+
: null;
|
|
15514
|
+
const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
|
|
15515
|
+
if (!yWord && !xWord) {
|
|
15516
|
+
return null;
|
|
15517
|
+
}
|
|
15518
|
+
return yWord && xWord
|
|
15519
|
+
? `${prefix}-${yWord}-${xWord}`
|
|
15520
|
+
: `${prefix}-${yWord || xWord}`;
|
|
15521
|
+
};
|
|
15522
|
+
|
|
15523
|
+
/**
|
|
15524
|
+
* Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
|
|
15525
|
+
* — picked for any real anchor, or for a point/corner placed dead-center
|
|
15526
|
+
* (both positionArea axes overlapping — there's no sensible direction to
|
|
15527
|
+
* slide from in that case). "sliding" otherwise. `anchor` is `undefined`
|
|
15528
|
+
* for any no-anchor/docked case (Dialog always, Popover's own custom
|
|
15529
|
+
* renderer when there's no real anchor), so this collapses to "scaling"
|
|
15530
|
+
* there only for the dead-center case, "sliding" otherwise. The two
|
|
15531
|
+
* "overlapping" booleans below describe the *positionArea* itself (a bare
|
|
15532
|
+
* word vs. "inset-"/"center"), not anything about the anchor — they'd
|
|
15533
|
+
* mean exactly the same thing even with no anchor at all, since it's the
|
|
15534
|
+
* position strategy, not the anchor, that decides whether there's a
|
|
15535
|
+
* direction to slide from.
|
|
15536
|
+
*/
|
|
15537
|
+
const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
|
|
15538
|
+
const yIsOverlapping =
|
|
15539
|
+
parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
|
|
15540
|
+
const xIsOverlapping =
|
|
15541
|
+
parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
|
|
15542
|
+
return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
|
|
15543
|
+
};
|
|
15544
|
+
|
|
15545
|
+
/**
|
|
15546
|
+
* A popup reads "outside" from its own border box: what a press lands on
|
|
15547
|
+
* decides nothing on its own (a genuine backdrop press and a press on the
|
|
15548
|
+
* popup's padding both report the popup element as their target, there being
|
|
15549
|
+
* no real ::backdrop node to be one), so the rectangle is what tells them
|
|
15550
|
+
* apart. That holds as long as the box and what the popup paints are the same
|
|
15551
|
+
* thing — which stops being true for a popup with no surface of its own
|
|
15552
|
+
* (`backgroundColor="transparent"`, no shadow, no padding): what the eye reads
|
|
15553
|
+
* as backdrop is then inside the box, and a press there stays a press on the
|
|
15554
|
+
* popup.
|
|
15555
|
+
*
|
|
15556
|
+
* `data-navi-popup-outside` is how a caller says which of its own boxes are
|
|
15557
|
+
* not the surface. It is opt-in because navi cannot infer it — a background
|
|
15558
|
+
* can come from anywhere — while the caller who made the popup see-through
|
|
15559
|
+
* knows exactly which box is decoration and which is paper.
|
|
15560
|
+
*
|
|
15561
|
+
* The marker answers for the element it is on, never for its descendants: a
|
|
15562
|
+
* box painted inside a marked one is still surface, so a row can be marked and
|
|
15563
|
+
* its empty halves read as backdrop while presses on the controls it holds do
|
|
15564
|
+
* not dismiss anything. A descendant with `pointer-events: none` never becomes
|
|
15565
|
+
* a press target at all, so the marked box answers in its place — while the
|
|
15566
|
+
* same declaration on the marked box itself takes it out of hit-testing and
|
|
15567
|
+
* makes the marker unreachable (see warnAboutUnreachableOutsideRegions).
|
|
15568
|
+
*/
|
|
15569
|
+
const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
|
|
15570
|
+
|
|
15571
|
+
/**
|
|
15572
|
+
* `data-navi-popup-inside` is the other direction: a box of the PAGE the
|
|
15573
|
+
* caller says belongs to a popup, so a press on it is not outside that popup
|
|
15574
|
+
* even though it lies beyond the border box — a card whose press is what
|
|
15575
|
+
* fills the one panel of a board. Its value names the popup by `id`, the way
|
|
15576
|
+
* `commandfor` does (several ids, space separated), because the exemption is
|
|
15577
|
+
* one popup's: a menu open beside the board still closes on the same press.
|
|
15578
|
+
*
|
|
15579
|
+
* Unlike the outside marker it answers for its whole subtree: the outside
|
|
15580
|
+
* marker carves free space out of a surface whose controls stay surface, this
|
|
15581
|
+
* one adds a whole thing — the card, its count, its buttons — to the popup's
|
|
15582
|
+
* own ground, and nothing in it is a dismissal.
|
|
15583
|
+
*/
|
|
15584
|
+
const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
|
|
15585
|
+
|
|
15586
|
+
/**
|
|
15587
|
+
* `data-navi-popup-group` names the peers of a popup: the triggers of the other
|
|
15588
|
+
* popups a press goes to from this one — a menu bar, where pressing another
|
|
15589
|
+
* menu while one is open switches to it, in that one press. A trigger joins by
|
|
15590
|
+
* carrying the name (a Picker's `popupGroup` writes it on its own), and a popup
|
|
15591
|
+
* reads it off what the press landed on.
|
|
15592
|
+
*
|
|
15593
|
+
* Returns the peer `target` is in, or null.
|
|
15594
|
+
*/
|
|
15595
|
+
const findPopupPeer = (target, group) => {
|
|
15596
|
+
if (!group || !target || typeof target.closest !== "function") {
|
|
15597
|
+
return null;
|
|
15598
|
+
}
|
|
15599
|
+
return target.closest(`[data-navi-popup-group="${CSS.escape(group)}"]`);
|
|
15600
|
+
};
|
|
15601
|
+
|
|
15602
|
+
/**
|
|
15603
|
+
* The click a wall took, handed to the peer (see findPopupPeer) it covered.
|
|
15604
|
+
*
|
|
15605
|
+
* A wall cannot let one press through and not another: it is what the pointer
|
|
15606
|
+
* hits, and that is the whole of how it spends a press. So it goes on taking
|
|
15607
|
+
* every one — the page behind stays as unhoverable and unpressable as it looks
|
|
15608
|
+
* — and once the press is over, its click is re-aimed at what the wall stood
|
|
15609
|
+
* over at that point, when that is a peer. The click alone, for the reason
|
|
15610
|
+
* transition_press.js gives: a pointer stream made up with no pointer behind it
|
|
15611
|
+
* starts gestures (a hold, a drag) that nothing ever ends, and a click is what
|
|
15612
|
+
* opens a popup's trigger.
|
|
15613
|
+
*
|
|
15614
|
+
* Called with the click that takes the wall down after a press closed the
|
|
15615
|
+
* popup (see armPointerDownOutsideClose), so the peer opens as the popup it
|
|
15616
|
+
* replaces goes.
|
|
15617
|
+
*/
|
|
15618
|
+
const handWallClickToPeer = (
|
|
15619
|
+
clickEvent,
|
|
15620
|
+
{ wallEl, popupEl, anchorEl, group },
|
|
15621
|
+
) => {
|
|
15622
|
+
if (!clickEvent.isTrusted || clickEvent.target !== wallEl) {
|
|
15623
|
+
// A click dispatched by hand is already aimed, and one that did not land
|
|
15624
|
+
// on the wall (a press on the popup's own outside region) was no wall's.
|
|
15625
|
+
return;
|
|
15626
|
+
}
|
|
15627
|
+
// The topmost element under the wall at that point: what the press would
|
|
15628
|
+
// have landed on with no wall. Asked past the wall rather than after hiding
|
|
15629
|
+
// it, which a fading wall may not be yet.
|
|
15630
|
+
const covered = document
|
|
15631
|
+
.elementsFromPoint(clickEvent.clientX, clickEvent.clientY)
|
|
15632
|
+
.find((el) => el !== wallEl && !popupEl.contains(el));
|
|
15633
|
+
const peer = findPopupPeer(covered, group);
|
|
15634
|
+
if (!peer) {
|
|
15635
|
+
return;
|
|
15636
|
+
}
|
|
15637
|
+
if (peer.contains(popupEl) || (anchorEl && peer.contains(anchorEl))) {
|
|
15638
|
+
// The popup's own trigger — the one holding it (a picker renders its
|
|
15639
|
+
// popup inside itself) or the one it opened against: the press through
|
|
15640
|
+
// the wall closed the popup, and handing the trigger its click would open
|
|
15641
|
+
// it again.
|
|
15642
|
+
return;
|
|
15643
|
+
}
|
|
15644
|
+
// The page sees one click, on the peer, rather than two.
|
|
15645
|
+
clickEvent.stopPropagation();
|
|
15646
|
+
// A PointerEvent where the browser makes clicks of them, a MouseEvent
|
|
15647
|
+
// elsewhere: the same kind, copied field by field.
|
|
15648
|
+
const ClickEvent = clickEvent.constructor;
|
|
15649
|
+
covered.dispatchEvent(new ClickEvent(clickEvent.type, clickEvent));
|
|
15650
|
+
};
|
|
15651
|
+
|
|
15652
|
+
/**
|
|
15653
|
+
* What a press landing on a region the caller declared as not-its-surface
|
|
15654
|
+
* does: exactly what the same press on the backdrop would do.
|
|
15655
|
+
*
|
|
15656
|
+
* Lives on the popup's own content element rather than in the backdrop's
|
|
15657
|
+
* handler, for both renderers: the backdrop is a sibling behind the popup
|
|
15658
|
+
* (never an ancestor), so a press inside the popup's box never reaches it.
|
|
15659
|
+
*/
|
|
15660
|
+
const handlePressOnOutsideRegion = (
|
|
15661
|
+
mouseDownEvent,
|
|
15662
|
+
{ popupEl, openController, pressOutside },
|
|
15663
|
+
) => {
|
|
15664
|
+
if (mouseDownEvent.button !== 0) {
|
|
15665
|
+
return;
|
|
15666
|
+
}
|
|
15667
|
+
const { target } = mouseDownEvent;
|
|
15668
|
+
if (!target.hasAttribute(OUTSIDE_REGION_ATTRIBUTE)) {
|
|
15669
|
+
return;
|
|
15670
|
+
}
|
|
15671
|
+
if (openedDuringThisPress(openController)) {
|
|
15672
|
+
// The release of the press that opened it is not somebody dismissing it
|
|
15673
|
+
// (see openedDuringThisPress).
|
|
15674
|
+
return;
|
|
15675
|
+
}
|
|
15676
|
+
// A popup opens inside its opener's own subtree, so a press on a region
|
|
15677
|
+
// belonging to a popup nested in this one bubbles through here too — and it
|
|
15678
|
+
// is a press on what is in front, not on this popup's own decoration.
|
|
15679
|
+
if (
|
|
15680
|
+
target.closest(`[navi-control="dialog"], [navi-control="popover"]`) !==
|
|
15681
|
+
popupEl
|
|
15682
|
+
) {
|
|
15683
|
+
return;
|
|
15684
|
+
}
|
|
15685
|
+
if (pressOutside === "capture") {
|
|
15686
|
+
mouseDownEvent.preventDefault();
|
|
15687
|
+
return;
|
|
15688
|
+
}
|
|
15689
|
+
if (pressOutside === "close" || pressOutside === "cancel") {
|
|
15690
|
+
openController.requestClose(mouseDownEvent, {
|
|
15691
|
+
isCancel: pressOutside === "cancel",
|
|
15692
|
+
});
|
|
15693
|
+
}
|
|
15694
|
+
};
|
|
15695
|
+
|
|
15696
|
+
/**
|
|
15697
|
+
* Hears an outside press from the document itself, for a popup with no
|
|
15698
|
+
* backdrop of its own to catch one: a modal `<dialog>` (whose native
|
|
15699
|
+
* `::backdrop` dispatches nothing a handler can be put on) and any popup the
|
|
15700
|
+
* caller asked to leave the page reachable (`backdrop={false}`).
|
|
15701
|
+
*
|
|
15702
|
+
* The press is read and never taken: no `preventDefault`, no
|
|
15703
|
+
* `stopPropagation`, and the listener is on `document` rather than on
|
|
15704
|
+
* anything covering the page — so what the press landed on hears it too, in
|
|
15705
|
+
* the same gesture, and the popup only takes note. That is the whole point of
|
|
15706
|
+
* a popup with no wall: a press outside is a press on the page, and the page
|
|
15707
|
+
* answers it. The one popup where that cannot hold is the modal dialog, where
|
|
15708
|
+
* the browser has already made everything behind inert before this runs.
|
|
15709
|
+
*
|
|
15710
|
+
* `pressEventType` says which event IS the press, and the two callers want
|
|
15711
|
+
* opposite ones:
|
|
15712
|
+
*
|
|
15713
|
+
* - `"pointerdown"` for a popup with no wall: a page with no wall over it is
|
|
15714
|
+
* a page whose own elements arbitrate their presses, and cancelling a
|
|
15715
|
+
* `pointerdown` — what a drag source and a control keeping the focus where
|
|
15716
|
+
* it is both do — suppresses every compatibility mouse event that would have
|
|
15717
|
+
* followed it. The press happens, reaches its target, and no `mousedown` is
|
|
15718
|
+
* ever dispatched for it. Reading the pointer event is reading the press
|
|
15719
|
+
* itself, whatever anything does with it afterwards, and it is the same
|
|
15720
|
+
* event the drag and surface detectors read.
|
|
15721
|
+
* - `"mousedown"` for the modal dialog, whose native `::backdrop` exists to
|
|
15722
|
+
* spend the press. The page's controls act on `mousedown`, and on a touch
|
|
15723
|
+
* screen that is not the same moment as `pointerdown`: the compatibility
|
|
15724
|
+
* mouse events are synthesized at `touchend` and hit-tested again then.
|
|
15725
|
+
* Closing on `pointerdown` takes the `::backdrop` away before that second
|
|
15726
|
+
* hit-test, so the `mousedown` lands on whatever the page holds under the
|
|
15727
|
+
* finger, and one tap both dismisses the dialog and presses the page. Read
|
|
15728
|
+
* on `mousedown`, the wall is still up when the press the page acts on is
|
|
15729
|
+
* aimed, and it catches it. Nothing behind a modal wall can cancel that
|
|
15730
|
+
* press, since nothing behind it hears the `pointerdown` at all.
|
|
15731
|
+
*
|
|
15732
|
+
* Capture phase, so a handler downstream that stops propagation cannot keep
|
|
15733
|
+
* the popup open either — the press is still outside whatever is made of it.
|
|
15734
|
+
*
|
|
15735
|
+
* Returns a cleanup that removes the listener.
|
|
15736
|
+
*/
|
|
15737
|
+
const armOutsidePressClose = (
|
|
15738
|
+
popupEl,
|
|
15739
|
+
{ openController, pressOutside, pressEventType = "pointerdown" },
|
|
15740
|
+
) => {
|
|
15741
|
+
const onDocumentPointerDown = (pointerDownEvent) => {
|
|
15742
|
+
if (pointerDownEvent.button !== 0) {
|
|
15743
|
+
return;
|
|
15744
|
+
}
|
|
15745
|
+
if (openedDuringThisPress(openController)) {
|
|
15746
|
+
// The release of the press that opened it is not somebody dismissing it
|
|
15747
|
+
// (see openedDuringThisPress).
|
|
15748
|
+
return;
|
|
15749
|
+
}
|
|
15750
|
+
// The press landed inside another popup: that is a press on what is in
|
|
15751
|
+
// front, not outside. Asking the target where it lives rather than asking
|
|
15752
|
+
// this popup whether it was pushed — a popup in front does not have to be
|
|
15753
|
+
// one this popup knows about. In front means neither inside nor around: a
|
|
15754
|
+
// popup nested in this one falls to the containment check below as the
|
|
15755
|
+
// inside press it is, and one this popup opened INSIDE (a bubble rendered
|
|
15756
|
+
// in a sheet's content) is around it, so what that popup holds is outside
|
|
15757
|
+
// this one exactly like the rest of the page.
|
|
15758
|
+
const popupUnderPointer = pointerDownEvent.target.closest?.(
|
|
15759
|
+
`[navi-control="dialog"], [navi-control="popover"]`,
|
|
15760
|
+
);
|
|
15761
|
+
if (
|
|
15762
|
+
popupUnderPointer &&
|
|
15763
|
+
popupUnderPointer !== popupEl &&
|
|
15764
|
+
!popupEl.contains(popupUnderPointer) &&
|
|
15765
|
+
!popupUnderPointer.contains(popupEl)
|
|
15766
|
+
) {
|
|
15767
|
+
return;
|
|
15768
|
+
}
|
|
15769
|
+
if (
|
|
15770
|
+
popupEl.id &&
|
|
15771
|
+
pointerDownEvent.target.closest?.(
|
|
15772
|
+
`[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupEl.id)}"]`,
|
|
15773
|
+
)
|
|
15774
|
+
) {
|
|
15775
|
+
return;
|
|
15776
|
+
}
|
|
15777
|
+
// Real DOM containment wins over the rectangle below — an element
|
|
15778
|
+
// genuinely inside the popup (`overflow: visible`, a negative margin, an
|
|
15779
|
+
// absolutely-positioned child) can be painted outside its border box, and
|
|
15780
|
+
// a press there is not outside just because its coordinates are. The
|
|
15781
|
+
// popup element itself is excluded (contains() answers true for it): a
|
|
15782
|
+
// press on a modal dialog's native backdrop reports the dialog as its
|
|
15783
|
+
// target, there being no `::backdrop` node to be one, so treating that as
|
|
15784
|
+
// contained would keep the rectangle from ever running.
|
|
15785
|
+
if (
|
|
15786
|
+
pointerDownEvent.target !== popupEl &&
|
|
15787
|
+
popupEl.contains(pointerDownEvent.target)
|
|
15788
|
+
) {
|
|
15789
|
+
return;
|
|
15790
|
+
}
|
|
15791
|
+
const rect = popupEl.getBoundingClientRect();
|
|
15792
|
+
const isOutside =
|
|
15793
|
+
pointerDownEvent.clientX < rect.left ||
|
|
15794
|
+
pointerDownEvent.clientX > rect.right ||
|
|
15795
|
+
pointerDownEvent.clientY < rect.top ||
|
|
15796
|
+
pointerDownEvent.clientY > rect.bottom;
|
|
15797
|
+
if (!isOutside) {
|
|
15798
|
+
return;
|
|
15799
|
+
}
|
|
15800
|
+
openController.requestClose(pointerDownEvent, {
|
|
15801
|
+
isCancel: pressOutside === "cancel",
|
|
15802
|
+
});
|
|
15803
|
+
};
|
|
15804
|
+
document.addEventListener(pressEventType, onDocumentPointerDown, {
|
|
15805
|
+
capture: true,
|
|
15806
|
+
});
|
|
15807
|
+
return () => {
|
|
15808
|
+
document.removeEventListener(pressEventType, onDocumentPointerDown, {
|
|
15809
|
+
capture: true,
|
|
15810
|
+
});
|
|
15811
|
+
};
|
|
15812
|
+
};
|
|
15813
|
+
|
|
15248
15814
|
/**
|
|
15249
15815
|
* Typography reset shared by the surfaces a control opens: callout, popover,
|
|
15250
15816
|
* dialog.
|
|
@@ -15465,6 +16031,24 @@ let calloutCount = 0;
|
|
|
15465
16031
|
// large enough to survive the pixel or two a trackpad rests on.
|
|
15466
16032
|
const CLOSE_BY_SCROLL_DISTANCE = 10;
|
|
15467
16033
|
|
|
16034
|
+
// The events of a press a `backdrop` callout spends, up to its click (which
|
|
16035
|
+
// closes the callout and is spent there), and `pointercancel`, which says the
|
|
16036
|
+
// browser took the press. No touch events: they are the scroll's, and what
|
|
16037
|
+
// listens to them — a list learning the user took its scroll over — is
|
|
16038
|
+
// watching the finger, not answering the press.
|
|
16039
|
+
const SPENT_PRESS_EVENT_TYPES = [
|
|
16040
|
+
"pointerdown",
|
|
16041
|
+
"pointerup",
|
|
16042
|
+
"pointercancel",
|
|
16043
|
+
"mousedown",
|
|
16044
|
+
"mouseup",
|
|
16045
|
+
"contextmenu",
|
|
16046
|
+
];
|
|
16047
|
+
// Where a press lands on something drawn over the page rather than on the page
|
|
16048
|
+
// itself: a popup of navi's (either layer) or a native top-layer element.
|
|
16049
|
+
const LAYER_SELECTOR =
|
|
16050
|
+
'[popover], dialog, [navi-control="popover"], [navi-control="dialog"]';
|
|
16051
|
+
|
|
15468
16052
|
const css$14 = /* css */ `
|
|
15469
16053
|
/* jsenv-css-opaque: same as dialog.jsx, cascade order. */
|
|
15470
16054
|
@layer navi {
|
|
@@ -15753,6 +16337,17 @@ const css$14 = /* css */ `
|
|
|
15753
16337
|
* the callout opened, so scrolling back cancels nothing but a round trip shorter than the
|
|
15754
16338
|
* distance leaves it open. Unlike an outside click it also dismisses an "error" callout: the
|
|
15755
16339
|
* caller asked for it explicitly.
|
|
16340
|
+
* @param {boolean} [options.backdrop=false] - Spend an outside press on closing the callout,
|
|
16341
|
+
* the way a popup's wall spends it: no link followed, no control pressed, no focus moved, no
|
|
16342
|
+
* hold or drag started under it. Without it that press closes the callout and is answered by
|
|
16343
|
+
* what it landed on too. Not a wall, though: the scroll a finger starts beside the callout
|
|
16344
|
+
* still scrolls, a press on a layer drawn over the page (a sheet the callout's content
|
|
16345
|
+
* opened) is that layer's, and a press that does not close the callout (an "error" one) is
|
|
16346
|
+
* not spent either
|
|
16347
|
+
* @param {string} [options.group] - With `backdrop`: the name the callout's peers carry in
|
|
16348
|
+
* `data-navi-popup-group` — the triggers of other popups, callouts or not. A press inside a
|
|
16349
|
+
* peer still closes the callout but is not spent, so the peer it landed on opens its own
|
|
16350
|
+
* popup in the same press — a menu bar, where pressing another menu switches to it
|
|
15756
16351
|
* @param {Event} [options.openingEvent] - The gesture the callout belongs to. It names the
|
|
15757
16352
|
* opener — `currentTarget` while the event is still dispatching, `target` once it is over,
|
|
15758
16353
|
* so awaiting before opening changes nothing — and the opener is the one part of the anchor
|
|
@@ -15817,6 +16412,8 @@ const openCallout = (
|
|
|
15817
16412
|
closeByPressOutside = status === "info",
|
|
15818
16413
|
closeOnFocusLeave = closeByPressOutside,
|
|
15819
16414
|
closeByScroll = false,
|
|
16415
|
+
backdrop = false,
|
|
16416
|
+
group,
|
|
15820
16417
|
closeButton = true,
|
|
15821
16418
|
openingEvent,
|
|
15822
16419
|
reopen = "toggle",
|
|
@@ -15901,6 +16498,9 @@ const openCallout = (
|
|
|
15901
16498
|
reason,
|
|
15902
16499
|
});
|
|
15903
16500
|
};
|
|
16501
|
+
// Asked again at each press rather than once: `update` changes both.
|
|
16502
|
+
const pressOutsideCloses = () =>
|
|
16503
|
+
closeByPressOutside && callout.status !== "error";
|
|
15904
16504
|
const onRequestClose = (event) => {
|
|
15905
16505
|
if (!callout.opened) {
|
|
15906
16506
|
return;
|
|
@@ -15909,10 +16509,7 @@ const openCallout = (
|
|
|
15909
16509
|
const clickOrSpaceOutside =
|
|
15910
16510
|
reason === "click_outside" || reason === "space_outside";
|
|
15911
16511
|
if (clickOrSpaceOutside) {
|
|
15912
|
-
if (!
|
|
15913
|
-
return;
|
|
15914
|
-
}
|
|
15915
|
-
if (callout.status === "error") {
|
|
16512
|
+
if (!pressOutsideCloses()) {
|
|
15916
16513
|
return;
|
|
15917
16514
|
}
|
|
15918
16515
|
} else if (reason === "focus_leave") {
|
|
@@ -16190,16 +16787,36 @@ const openCallout = (
|
|
|
16190
16787
|
}
|
|
16191
16788
|
return openerElement === target || openerElement.contains(target);
|
|
16192
16789
|
};
|
|
16790
|
+
// Whether an outside press on `target` is spent on closing the callout
|
|
16791
|
+
// (`backdrop`). What is not the page behind the callout keeps its press: a
|
|
16792
|
+
// layer the callout is not part of — its own page may well be a sheet, a
|
|
16793
|
+
// sheet its content opened is in front of it — and a peer, which the press
|
|
16794
|
+
// is meant to open.
|
|
16795
|
+
const spendsPressOn = (target) => {
|
|
16796
|
+
if (!backdrop || !pressOutsideCloses()) {
|
|
16797
|
+
return false;
|
|
16798
|
+
}
|
|
16799
|
+
const layer = target.closest?.(LAYER_SELECTOR);
|
|
16800
|
+
if (layer && !layer.contains(calloutElement)) {
|
|
16801
|
+
return false;
|
|
16802
|
+
}
|
|
16803
|
+
if (findPopupPeer(target, group)) {
|
|
16804
|
+
return false;
|
|
16805
|
+
}
|
|
16806
|
+
return true;
|
|
16807
|
+
};
|
|
16808
|
+
const spend = (event) => {
|
|
16809
|
+
debug(event, `outside press spent on closing the callout (backdrop)`);
|
|
16810
|
+
event.preventDefault();
|
|
16811
|
+
event.stopPropagation();
|
|
16812
|
+
};
|
|
16193
16813
|
const handleClickOutside = (event) => {
|
|
16194
16814
|
if (event.button !== 0) {
|
|
16195
16815
|
// right click
|
|
16196
16816
|
return;
|
|
16197
16817
|
}
|
|
16198
16818
|
const clickTarget = event.target;
|
|
16199
|
-
if (
|
|
16200
|
-
clickTarget === calloutElement ||
|
|
16201
|
-
calloutElement.contains(clickTarget)
|
|
16202
|
-
) {
|
|
16819
|
+
if (calloutElement.contains(clickTarget)) {
|
|
16203
16820
|
return;
|
|
16204
16821
|
}
|
|
16205
16822
|
if (isInsideOpener(clickTarget)) {
|
|
@@ -16212,14 +16829,20 @@ const openCallout = (
|
|
|
16212
16829
|
debug(event, `click on opener, let its handler decide`);
|
|
16213
16830
|
return;
|
|
16214
16831
|
}
|
|
16832
|
+
// Read before the close, which takes the callout out of the document —
|
|
16833
|
+
// and out of the layer it stands in, for spendsPressOn to find.
|
|
16834
|
+
const spent = spendsPressOn(clickTarget);
|
|
16215
16835
|
requestClose(event, "click_outside");
|
|
16836
|
+
if (spent) {
|
|
16837
|
+
spend(event);
|
|
16838
|
+
}
|
|
16216
16839
|
};
|
|
16217
16840
|
const handleSpaceOutside = (event) => {
|
|
16218
16841
|
if (event.key !== " ") {
|
|
16219
16842
|
return;
|
|
16220
16843
|
}
|
|
16221
16844
|
const keyTarget = event.target;
|
|
16222
|
-
if (
|
|
16845
|
+
if (calloutElement.contains(keyTarget)) {
|
|
16223
16846
|
return;
|
|
16224
16847
|
}
|
|
16225
16848
|
if (isInsideOpener(keyTarget)) {
|
|
@@ -16228,7 +16851,41 @@ const openCallout = (
|
|
|
16228
16851
|
debug(event, `space on opener, let its handler decide`);
|
|
16229
16852
|
return;
|
|
16230
16853
|
}
|
|
16854
|
+
const spent = spendsPressOn(keyTarget);
|
|
16231
16855
|
requestClose(event, "space_outside");
|
|
16856
|
+
if (spent) {
|
|
16857
|
+
spend(event);
|
|
16858
|
+
}
|
|
16859
|
+
};
|
|
16860
|
+
// The press before its click: a click ends a gesture whose pointerdown
|
|
16861
|
+
// already reached the page, and what acts on the press itself — a hold, a
|
|
16862
|
+
// drag, the focus moving — would otherwise happen under a callout that only
|
|
16863
|
+
// closes on the click. Decided at the pointerdown and kept for the whole
|
|
16864
|
+
// press, wherever its later events land: a release beside the callout ends
|
|
16865
|
+
// a drag started inside it, and must reach it.
|
|
16866
|
+
let pressSpent = false;
|
|
16867
|
+
const handlePressOutside = (event) => {
|
|
16868
|
+
if (event.type === "pointerdown") {
|
|
16869
|
+
const pressTarget = event.target;
|
|
16870
|
+
pressSpent =
|
|
16871
|
+
event.button === 0 &&
|
|
16872
|
+
!calloutElement.contains(pressTarget) &&
|
|
16873
|
+
!isInsideOpener(pressTarget) &&
|
|
16874
|
+
spendsPressOn(pressTarget);
|
|
16875
|
+
} else if (event.type === "pointercancel") {
|
|
16876
|
+
// The browser took the press over — a scroll — and nothing follows it.
|
|
16877
|
+
pressSpent = false;
|
|
16878
|
+
return;
|
|
16879
|
+
}
|
|
16880
|
+
if (!pressSpent) {
|
|
16881
|
+
return;
|
|
16882
|
+
}
|
|
16883
|
+
if (event.type === "mousedown" || event.type === "contextmenu") {
|
|
16884
|
+
// What the press does on its own: the focus moving, a selection
|
|
16885
|
+
// starting, the menu of a held finger.
|
|
16886
|
+
event.preventDefault();
|
|
16887
|
+
}
|
|
16888
|
+
event.stopPropagation();
|
|
16232
16889
|
};
|
|
16233
16890
|
|
|
16234
16891
|
const registerClickOutsideListener = () => {
|
|
@@ -16238,6 +16895,16 @@ const openCallout = (
|
|
|
16238
16895
|
document.removeEventListener("click", handleClickOutside, true);
|
|
16239
16896
|
document.removeEventListener("keydown", handleSpaceOutside, true);
|
|
16240
16897
|
});
|
|
16898
|
+
if (backdrop) {
|
|
16899
|
+
for (const type of SPENT_PRESS_EVENT_TYPES) {
|
|
16900
|
+
document.addEventListener(type, handlePressOutside, true);
|
|
16901
|
+
}
|
|
16902
|
+
addTeardown(() => {
|
|
16903
|
+
for (const type of SPENT_PRESS_EVENT_TYPES) {
|
|
16904
|
+
document.removeEventListener(type, handlePressOutside, true);
|
|
16905
|
+
}
|
|
16906
|
+
});
|
|
16907
|
+
}
|
|
16241
16908
|
};
|
|
16242
16909
|
// A callout opened during a press (mousedown or pointerdown) must wait for the
|
|
16243
16910
|
// matching release before listening for click-outside, otherwise the same
|
|
@@ -17351,7 +18018,7 @@ const generateSvgWithoutArrow = (width, height) => {
|
|
|
17351
18018
|
*
|
|
17352
18019
|
* Usage:
|
|
17353
18020
|
* const myToken = createOpenToken();
|
|
17354
|
-
* calloutManager.addOpenToken(myToken, { message, status, testId, closeByScroll, anchorElement, event, skipFocus, onClose });
|
|
18021
|
+
* calloutManager.addOpenToken(myToken, { message, status, testId, closeByScroll, backdrop, group, anchorElement, event, skipFocus, onClose });
|
|
17355
18022
|
* calloutManager.removeOpenToken(myToken, event);
|
|
17356
18023
|
* calloutManager.requestCloseCallout(event, debugReason); // force-close all
|
|
17357
18024
|
* calloutManager.callout // current open callout or null
|
|
@@ -17409,6 +18076,8 @@ const createCalloutManager = (
|
|
|
17409
18076
|
closeButton: tokenData.closeButton,
|
|
17410
18077
|
closeByPressOutside: tokenData.status !== "error",
|
|
17411
18078
|
closeByScroll: tokenData.closeByScroll,
|
|
18079
|
+
backdrop: tokenData.backdrop,
|
|
18080
|
+
group: tokenData.group,
|
|
17412
18081
|
anchorElement,
|
|
17413
18082
|
openingEvent: event,
|
|
17414
18083
|
skipFocus: tokenData.skipFocus,
|
|
@@ -17537,6 +18206,8 @@ const createCalloutManager = (
|
|
|
17537
18206
|
icon,
|
|
17538
18207
|
closeButton,
|
|
17539
18208
|
closeByScroll,
|
|
18209
|
+
backdrop,
|
|
18210
|
+
group,
|
|
17540
18211
|
anchorElement,
|
|
17541
18212
|
event,
|
|
17542
18213
|
skipFocus,
|
|
@@ -17554,6 +18225,8 @@ const createCalloutManager = (
|
|
|
17554
18225
|
icon,
|
|
17555
18226
|
closeButton,
|
|
17556
18227
|
closeByScroll,
|
|
18228
|
+
backdrop,
|
|
18229
|
+
group,
|
|
17557
18230
|
skipFocus,
|
|
17558
18231
|
onClose,
|
|
17559
18232
|
// Resolved as the token is added, and kept with it: a token shown later
|
|
@@ -23629,7 +24302,31 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
23629
24302
|
}
|
|
23630
24303
|
return result;
|
|
23631
24304
|
};
|
|
23632
|
-
|
|
24305
|
+
// A computed of the row with this id, one per id: what a render reads to
|
|
24306
|
+
// follow that row and no other. An upsert replaces only the items it changes
|
|
24307
|
+
// (see assign below), so a row nobody wrote keeps its object, and its
|
|
24308
|
+
// computed stays still while the rest of the store moves — read through
|
|
24309
|
+
// `select`, a render follows the whole store. Kept as long as the store,
|
|
24310
|
+
// like the rows it holds.
|
|
24311
|
+
const itemSignalById = new Map();
|
|
24312
|
+
const itemSignalForId = (id) => {
|
|
24313
|
+
let itemSignal = itemSignalById.get(id);
|
|
24314
|
+
if (!itemSignal) {
|
|
24315
|
+
itemSignal = computed(() => idMapSignal.value.get(id));
|
|
24316
|
+
itemSignalById.set(id, itemSignal);
|
|
24317
|
+
}
|
|
24318
|
+
return itemSignal;
|
|
24319
|
+
};
|
|
24320
|
+
// A value changes a row when it is another object — the app writing a new
|
|
24321
|
+
// one says something changed, and that is taken as said. An answer says it
|
|
24322
|
+
// with every object nested in its rows: JSON.parse makes them all new, the
|
|
24323
|
+
// ones that did not move included, so a revalidation returning the same
|
|
24324
|
+
// rows would replace every one of them for every reader. What an answer
|
|
24325
|
+
// sends again equal in depth is the value already held (see upsertAnswer).
|
|
24326
|
+
const valueChangedByWrite = (newValue, oldValue) => newValue !== oldValue;
|
|
24327
|
+
const valueChangedByAnswer = (newValue, oldValue) =>
|
|
24328
|
+
newValue !== oldValue && !compareTwoJsValues(newValue, oldValue);
|
|
24329
|
+
const upsertWith = (args, valueChanged) => {
|
|
23633
24330
|
const mutationsMap = new Map(); // Map<itemId, propertyMutations>
|
|
23634
24331
|
const triggerPropertyMutations = () => {
|
|
23635
24332
|
for (const itemPropertiesObserver of itemPropertiesObserverSet) {
|
|
@@ -23693,7 +24390,27 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
23693
24390
|
const newValue = props[key];
|
|
23694
24391
|
if (itemOwnKeys.includes(key)) {
|
|
23695
24392
|
const oldValue = item[key];
|
|
23696
|
-
|
|
24393
|
+
const { set } = itemOwnPropertyDescriptors[key];
|
|
24394
|
+
if (set) {
|
|
24395
|
+
// A relation (.one(), .many() and their scoped forms in
|
|
24396
|
+
// resource_graph.js): the row holds which children it points to,
|
|
24397
|
+
// the children live in their own store, and whoever reads them
|
|
24398
|
+
// follows them there. Its setter writes them and says whether the
|
|
24399
|
+
// row points elsewhere now. Compared as a value instead, the
|
|
24400
|
+
// children received are never the object the row hands out, and
|
|
24401
|
+
// every write naming the relation would replace the row.
|
|
24402
|
+
if (set.call(itemWithProps, newValue)) {
|
|
24403
|
+
hasChanges = true;
|
|
24404
|
+
propertyMutations[key] = {
|
|
24405
|
+
oldValue,
|
|
24406
|
+
newValue,
|
|
24407
|
+
target: item,
|
|
24408
|
+
newTarget: itemWithProps,
|
|
24409
|
+
};
|
|
24410
|
+
}
|
|
24411
|
+
continue;
|
|
24412
|
+
}
|
|
24413
|
+
if (valueChanged(newValue, oldValue)) {
|
|
23697
24414
|
hasChanges = true;
|
|
23698
24415
|
itemWithProps[key] = newValue;
|
|
23699
24416
|
propertyMutations[key] = {
|
|
@@ -23845,6 +24562,9 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
23845
24562
|
triggerPropertyMutations();
|
|
23846
24563
|
return item;
|
|
23847
24564
|
};
|
|
24565
|
+
const upsert = (...args) => upsertWith(args, valueChangedByWrite);
|
|
24566
|
+
// The same, for the result of a callback — what the server answered.
|
|
24567
|
+
const upsertAnswer = (...args) => upsertWith(args, valueChangedByAnswer);
|
|
23848
24568
|
const drop = (...args) => {
|
|
23849
24569
|
const removedItemArray = [];
|
|
23850
24570
|
const triggerRemovedMutations = () => {
|
|
@@ -24019,7 +24739,9 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
24019
24739
|
arraySignal,
|
|
24020
24740
|
select,
|
|
24021
24741
|
selectAll,
|
|
24742
|
+
itemSignalForId,
|
|
24022
24743
|
upsert,
|
|
24744
|
+
upsertAnswer,
|
|
24023
24745
|
drop,
|
|
24024
24746
|
|
|
24025
24747
|
observeItemProperties,
|
|
@@ -24777,7 +25499,7 @@ const createRangeReader = (
|
|
|
24777
25499
|
`${actionName} must return { items, start, count }, received ${describeResult(result)}.`,
|
|
24778
25500
|
);
|
|
24779
25501
|
}
|
|
24780
|
-
const items = store.
|
|
25502
|
+
const items = store.upsertAnswer(result.items);
|
|
24781
25503
|
let { start, count } = result;
|
|
24782
25504
|
if (start === undefined) {
|
|
24783
25505
|
const startAsked = rangeParams.start;
|
|
@@ -25184,12 +25906,34 @@ const createResource = (
|
|
|
25184
25906
|
idKey,
|
|
25185
25907
|
uniqueKeys,
|
|
25186
25908
|
|
|
25187
|
-
// Reactive reads, not hooks:
|
|
25188
|
-
//
|
|
25189
|
-
//
|
|
25909
|
+
// Reactive reads, not hooks: nothing hook-shaped inside, so a loop or a
|
|
25910
|
+
// condition is fine. Each subscribes the render that calls it to what it
|
|
25911
|
+
// returns: the rows asked for, each through a computed of that row (see
|
|
25912
|
+
// itemSignalForId in array_signal_store.js), or the whole store for
|
|
25913
|
+
// `useArray` — a card reading its own row stays still while the page next
|
|
25914
|
+
// to it loads rows of its own. `useAllByIds` skips the ids the store does
|
|
25915
|
+
// not hold.
|
|
25190
25916
|
useArray: () => store.arraySignal.value,
|
|
25191
|
-
useById: (id) => store.
|
|
25192
|
-
useAllByIds: (idArray) =>
|
|
25917
|
+
useById: (id) => store.itemSignalForId(id).value,
|
|
25918
|
+
useAllByIds: (idArray) => {
|
|
25919
|
+
const items = [];
|
|
25920
|
+
for (const idOrProps of idArray) {
|
|
25921
|
+
let id = idOrProps;
|
|
25922
|
+
if (isProps(idOrProps) && !Object.hasOwn(idOrProps, idKey)) {
|
|
25923
|
+
// Named by a unique key: which row that is can only be read off
|
|
25924
|
+
// the whole store.
|
|
25925
|
+
const item = store.select(idOrProps);
|
|
25926
|
+
id = item ? item[idKey] : undefined;
|
|
25927
|
+
} else if (isProps(idOrProps)) {
|
|
25928
|
+
id = idOrProps[idKey];
|
|
25929
|
+
}
|
|
25930
|
+
const item = store.itemSignalForId(id).value;
|
|
25931
|
+
if (item) {
|
|
25932
|
+
items.push(item);
|
|
25933
|
+
}
|
|
25934
|
+
}
|
|
25935
|
+
return items;
|
|
25936
|
+
},
|
|
25193
25937
|
|
|
25194
25938
|
withParams: undefined,
|
|
25195
25939
|
one: undefined,
|
|
@@ -25416,7 +26160,7 @@ const createResource = (
|
|
|
25416
26160
|
: // GET/PUT contract (see .one() JSDoc): the parent object with the
|
|
25417
26161
|
// relationship nested inside, or null for no relationship.
|
|
25418
26162
|
(result) => {
|
|
25419
|
-
const item = store.
|
|
26163
|
+
const item = store.upsertAnswer(result);
|
|
25420
26164
|
const childItem = item[propertyName];
|
|
25421
26165
|
return childItem ? childItem[childIdKey] : undefined;
|
|
25422
26166
|
};
|
|
@@ -25597,8 +26341,8 @@ const createResource = (
|
|
|
25597
26341
|
: (childData) => {
|
|
25598
26342
|
// an array is [property, value, props], used to rename the child id
|
|
25599
26343
|
const childItem = Array.isArray(childData)
|
|
25600
|
-
? childStore.
|
|
25601
|
-
: childStore.
|
|
26344
|
+
? childStore.upsertAnswer(...childData)
|
|
26345
|
+
: childStore.upsertAnswer(childData);
|
|
25602
26346
|
return childItem[childIdKey];
|
|
25603
26347
|
};
|
|
25604
26348
|
const throwInvalidResult = createInvalidResultThrower(
|
|
@@ -25640,7 +26384,7 @@ const createResource = (
|
|
|
25640
26384
|
? (result) => {
|
|
25641
26385
|
// GET_MANY contract (see .many() JSDoc): the parent object with
|
|
25642
26386
|
// the child array nested inside; the array replaces the relationship.
|
|
25643
|
-
const item = store.
|
|
26387
|
+
const item = store.upsertAnswer(result);
|
|
25644
26388
|
const childItemArray = item[propertyName];
|
|
25645
26389
|
return childItemArray.map((childItem) => childItem[childIdKey]);
|
|
25646
26390
|
}
|
|
@@ -25671,7 +26415,7 @@ const createResource = (
|
|
|
25671
26415
|
return deletedChildItemIdArray;
|
|
25672
26416
|
}
|
|
25673
26417
|
: (childDataArray) => {
|
|
25674
|
-
const childItemArray = childStore.
|
|
26418
|
+
const childItemArray = childStore.upsertAnswer(childDataArray);
|
|
25675
26419
|
return childItemArray.map((childItem) => childItem[childIdKey]);
|
|
25676
26420
|
};
|
|
25677
26421
|
const throwInvalidResult = createInvalidResultThrower(
|
|
@@ -25810,18 +26554,37 @@ const createResource = (
|
|
|
25810
26554
|
childSetup(childItem);
|
|
25811
26555
|
}
|
|
25812
26556
|
const childSignal = signal(null);
|
|
26557
|
+
// Returns whether the child changed — what the store asks a relation
|
|
26558
|
+
// (see assign in array_signal_store.js). Assigned in place, the child
|
|
26559
|
+
// tells nobody itself: a prop it receives changed is a change of the
|
|
26560
|
+
// owner's row, which is how its readers see it.
|
|
25813
26561
|
const applyProps = (props) => {
|
|
25814
26562
|
if (!props) {
|
|
26563
|
+
if (childSignal.peek() === null) {
|
|
26564
|
+
return false;
|
|
26565
|
+
}
|
|
25815
26566
|
childSignal.value = null;
|
|
25816
|
-
return;
|
|
26567
|
+
return true;
|
|
25817
26568
|
}
|
|
25818
|
-
|
|
26569
|
+
let changed = false;
|
|
26570
|
+
// Assign each prop in place. Reactive setters (from chained .one()
|
|
26571
|
+
// etc.) fire, and say whether their relation changed.
|
|
25819
26572
|
for (const [key, value] of Object.entries(props)) {
|
|
25820
|
-
|
|
26573
|
+
const descriptor = Object.getOwnPropertyDescriptor(childItem, key);
|
|
26574
|
+
if (descriptor && descriptor.set) {
|
|
26575
|
+
if (descriptor.set.call(childItem, value)) {
|
|
26576
|
+
changed = true;
|
|
26577
|
+
}
|
|
26578
|
+
} else if (childItem[key] !== value) {
|
|
26579
|
+
childItem[key] = value;
|
|
26580
|
+
changed = true;
|
|
26581
|
+
}
|
|
25821
26582
|
}
|
|
25822
26583
|
if (childSignal.peek() !== childItem) {
|
|
25823
26584
|
childSignal.value = childItem; // first activation: null → childItem
|
|
26585
|
+
changed = true;
|
|
25824
26586
|
}
|
|
26587
|
+
return changed;
|
|
25825
26588
|
};
|
|
25826
26589
|
if (!ownerName) {
|
|
25827
26590
|
// Declared on a scoped child, an owner id is only unique inside its
|
|
@@ -26109,7 +26872,7 @@ const createResource = (
|
|
|
26109
26872
|
if (isMany) {
|
|
26110
26873
|
// GET_MANY, POST_MANY, PUT_MANY etc: rest[0] is the array of items,
|
|
26111
26874
|
// and it replaces the whole collection.
|
|
26112
|
-
const itemArray = childStore.
|
|
26875
|
+
const itemArray = childStore.upsertAnswer(rest[0]);
|
|
26113
26876
|
const idArray = itemArray.map((childItem) => childItem[childIdKey]);
|
|
26114
26877
|
idArraySignal.value = idArray;
|
|
26115
26878
|
return [ownerId, idArray];
|
|
@@ -26118,8 +26881,8 @@ const createResource = (
|
|
|
26118
26881
|
// GET, POST, PUT, PATCH: rest may be [props] or [oldId, props] for renames
|
|
26119
26882
|
const childItem =
|
|
26120
26883
|
rest.length > 1
|
|
26121
|
-
? childStore.
|
|
26122
|
-
: childStore.
|
|
26884
|
+
? childStore.upsertAnswer(...rest)
|
|
26885
|
+
: childStore.upsertAnswer(rest[0]);
|
|
26123
26886
|
return [ownerId, childItem[childIdKey]];
|
|
26124
26887
|
},
|
|
26125
26888
|
valueToData: (value) => {
|
|
@@ -26255,8 +27018,8 @@ const createRestActionFactoryForRoot = (
|
|
|
26255
27018
|
// An array result is [property, value, props] — used to rename the
|
|
26256
27019
|
// idKey of an item: store.upsert("name", "currentName", { name: "newName" })
|
|
26257
27020
|
const item = Array.isArray(result)
|
|
26258
|
-
? store.
|
|
26259
|
-
: store.
|
|
27021
|
+
? store.upsertAnswer(...result)
|
|
27022
|
+
: store.upsertAnswer(result);
|
|
26260
27023
|
return item[idKey];
|
|
26261
27024
|
};
|
|
26262
27025
|
const throwInvalidResult = createInvalidResultThrower(
|
|
@@ -26320,7 +27083,7 @@ const createRestActionFactoryForRoot = (
|
|
|
26320
27083
|
verb === "DELETE"
|
|
26321
27084
|
? (idOrMutableIdArray) => store.drop(idOrMutableIdArray)
|
|
26322
27085
|
: (dataArray) => {
|
|
26323
|
-
const itemArray = store.
|
|
27086
|
+
const itemArray = store.upsertAnswer(dataArray);
|
|
26324
27087
|
return itemArray.map((item) => item[idKey]);
|
|
26325
27088
|
};
|
|
26326
27089
|
|
|
@@ -26449,14 +27212,17 @@ ${relationLabel} source location: ${declarationSite}`,
|
|
|
26449
27212
|
// array of child props/ids, or anything else meaning "empty") into an array of
|
|
26450
27213
|
// child ids, upserting each entry into the child store. The id array signal is
|
|
26451
27214
|
// only touched when the resulting ids actually differ.
|
|
27215
|
+
// Returns whether the children the row points to changed — what the store asks
|
|
27216
|
+
// a relation (see assign in array_signal_store.js).
|
|
26452
27217
|
const createChildIdArrayUpdater = (childStore, childIdKey, idArraySignal) => {
|
|
26453
27218
|
return (valueArray) => {
|
|
26454
27219
|
const currentIdArray = idArraySignal.peek();
|
|
26455
27220
|
if (!Array.isArray(valueArray)) {
|
|
26456
27221
|
if (currentIdArray.length > 0) {
|
|
26457
27222
|
idArraySignal.value = [];
|
|
27223
|
+
return true;
|
|
26458
27224
|
}
|
|
26459
|
-
return;
|
|
27225
|
+
return false;
|
|
26460
27226
|
}
|
|
26461
27227
|
const idArray = [];
|
|
26462
27228
|
let modified = false;
|
|
@@ -26478,7 +27244,9 @@ const createChildIdArrayUpdater = (childStore, childIdKey, idArraySignal) => {
|
|
|
26478
27244
|
}
|
|
26479
27245
|
if (modified || currentIdArray.length !== idArray.length) {
|
|
26480
27246
|
idArraySignal.value = idArray;
|
|
27247
|
+
return true;
|
|
26481
27248
|
}
|
|
27249
|
+
return false;
|
|
26482
27250
|
};
|
|
26483
27251
|
};
|
|
26484
27252
|
|
|
@@ -26896,6 +27664,24 @@ const TYPE_CONVERTERS = {
|
|
|
26896
27664
|
},
|
|
26897
27665
|
};
|
|
26898
27666
|
|
|
27667
|
+
/**
|
|
27668
|
+
* What a component can learn about the page it is rendered in, from the route
|
|
27669
|
+
* container rendering that page (see useContentKeepingLeavingPages in
|
|
27670
|
+
* route.jsx). `null` outside of any route container.
|
|
27671
|
+
*
|
|
27672
|
+
* - `isShown()` — whether the container would still show the page. False as
|
|
27673
|
+
* soon as the address leads elsewhere: the page is about to be taken down, or
|
|
27674
|
+
* hidden until the route transition leaving it is over, and nothing it reads
|
|
27675
|
+
* is worth a render any more. False for good once it is kept while leaving: a
|
|
27676
|
+
* return to its route mounts the page anew. Asked outside any render — by a
|
|
27677
|
+
* subscription deciding whether to re-render — and it reads the routes
|
|
27678
|
+
* untracked, so asking subscribes nobody to them.
|
|
27679
|
+
* - `leavingSignal` — true once the page is hidden and kept while its movement
|
|
27680
|
+
* plays: still mounted, so what it fills outside its own nodes (a slot in a
|
|
27681
|
+
* bar) steps out rather than showing with the page arriving.
|
|
27682
|
+
*/
|
|
27683
|
+
const RoutePageContext = createContext(null);
|
|
27684
|
+
|
|
26899
27685
|
const promiseStateWeakMap = new WeakMap();
|
|
26900
27686
|
const usePromiseAsyncData = (
|
|
26901
27687
|
promise,
|
|
@@ -27031,12 +27817,39 @@ const useActionAsyncData = (action, {
|
|
|
27031
27817
|
// has already processed the settlement and the detached DOM is discarded.
|
|
27032
27818
|
const runningState = action.runningStateSignal.peek();
|
|
27033
27819
|
const [, setTick] = useState(0);
|
|
27820
|
+
const routePage = useContext(RoutePageContext);
|
|
27034
27821
|
useEffect(() => {
|
|
27822
|
+
let stopOwingRender = null;
|
|
27823
|
+
const rerender = () => {
|
|
27824
|
+
// Not for a page the address has left: it is about to be taken down,
|
|
27825
|
+
// or hidden until the route transition leaving it is over, and its
|
|
27826
|
+
// route's action moving as that route stops matching would re-render
|
|
27827
|
+
// the whole page — inside the transition's update callback, where
|
|
27828
|
+
// every render delays the movement (see route_page_context.js).
|
|
27829
|
+
if (routePage && !routePage.isShown()) {
|
|
27830
|
+
// Owed rather than dropped: the address can come back before the
|
|
27831
|
+
// container renders (a redirect straight back), and the page then
|
|
27832
|
+
// stays on screen with what it read last.
|
|
27833
|
+
if (!stopOwingRender) {
|
|
27834
|
+
stopOwingRender = observeRouteRender(() => {
|
|
27835
|
+
if (routePage.isShown()) {
|
|
27836
|
+
rerender();
|
|
27837
|
+
}
|
|
27838
|
+
});
|
|
27839
|
+
}
|
|
27840
|
+
return;
|
|
27841
|
+
}
|
|
27842
|
+
if (stopOwingRender) {
|
|
27843
|
+
stopOwingRender();
|
|
27844
|
+
stopOwingRender = null;
|
|
27845
|
+
}
|
|
27846
|
+
setTick(n => n + 1);
|
|
27847
|
+
};
|
|
27035
27848
|
const unsubscribeFromRunningState = action.runningStateSignal.subscribe(state => {
|
|
27036
27849
|
if (state === RUNNING) {
|
|
27037
27850
|
dismissedActionWeakSet.delete(action);
|
|
27038
27851
|
}
|
|
27039
|
-
|
|
27852
|
+
rerender();
|
|
27040
27853
|
});
|
|
27041
27854
|
// The data does not come from this action's runs alone: dataSignal is a
|
|
27042
27855
|
// computed over the resource store, so an other action writing that store
|
|
@@ -27050,7 +27863,7 @@ const useActionAsyncData = (action, {
|
|
|
27050
27863
|
dataNotificationIsInitial = false;
|
|
27051
27864
|
return;
|
|
27052
27865
|
}
|
|
27053
|
-
|
|
27866
|
+
rerender();
|
|
27054
27867
|
});
|
|
27055
27868
|
// The params say WHICH question this is, and this hook reads them: to know
|
|
27056
27869
|
// there is nothing to ask for, and to start the run it owns. A binding
|
|
@@ -27063,7 +27876,7 @@ const useActionAsyncData = (action, {
|
|
|
27063
27876
|
paramsNotificationIsInitial = false;
|
|
27064
27877
|
return;
|
|
27065
27878
|
}
|
|
27066
|
-
|
|
27879
|
+
rerender();
|
|
27067
27880
|
});
|
|
27068
27881
|
// A debounced binding waits before it retargets, so nothing above changes
|
|
27069
27882
|
// while the delay runs — but what is on screen is already out of date.
|
|
@@ -27073,18 +27886,21 @@ const useActionAsyncData = (action, {
|
|
|
27073
27886
|
settlingNotificationIsInitial = false;
|
|
27074
27887
|
return;
|
|
27075
27888
|
}
|
|
27076
|
-
|
|
27889
|
+
rerender();
|
|
27077
27890
|
});
|
|
27078
27891
|
return () => {
|
|
27079
27892
|
unsubscribeFromRunningState();
|
|
27080
27893
|
unsubscribeFromData();
|
|
27081
27894
|
unsubscribeFromParams();
|
|
27082
27895
|
unsubscribeFromParamsSettling();
|
|
27896
|
+
if (stopOwingRender) {
|
|
27897
|
+
stopOwingRender();
|
|
27898
|
+
}
|
|
27083
27899
|
};
|
|
27084
27900
|
// Bound to the action, not to the mount: params given as a plain object
|
|
27085
27901
|
// make another action instance, and the component would otherwise stay
|
|
27086
27902
|
// subscribed to the state of the one it no longer reads.
|
|
27087
|
-
}, [action]);
|
|
27903
|
+
}, [action, routePage]);
|
|
27088
27904
|
|
|
27089
27905
|
// The params moved and the run has not started: under a debounce the action
|
|
27090
27906
|
// is still the previous one, COMPLETED with the previous answer. There is no
|
|
@@ -31185,13 +32001,6 @@ const keepLeavingPages = () => {
|
|
|
31185
32001
|
hold.containers.clear();
|
|
31186
32002
|
};
|
|
31187
32003
|
};
|
|
31188
|
-
|
|
31189
|
-
// What a page can learn about its place from the container rendering it:
|
|
31190
|
-
// whether it is a page kept while leaving (see keepLeavingPages). Such a page
|
|
31191
|
-
// is still mounted, and whatever it fills OUTSIDE its own nodes — a slot in a
|
|
31192
|
-
// bar, say — would otherwise go on showing with the page arriving (see
|
|
31193
|
-
// layout/slot.jsx).
|
|
31194
|
-
const RoutePageContext = createContext(null);
|
|
31195
32004
|
const LEAVING_ATTRIBUTE = "data-navi-route-leaving";
|
|
31196
32005
|
const css$13 = /* css */`[data-navi-route-leaving] {
|
|
31197
32006
|
display: none !important;
|
|
@@ -31316,7 +32125,21 @@ const RouteContainer = ({
|
|
|
31316
32125
|
shownBranchRef.current = activeBranch;
|
|
31317
32126
|
}
|
|
31318
32127
|
const branch = shownBranchRef.current || activeBranch;
|
|
31319
|
-
|
|
32128
|
+
// What this container would show if it rendered now, by the same rule as
|
|
32129
|
+
// the render above — asked from outside any render (see isShown below).
|
|
32130
|
+
const childrenRef = useRef(null);
|
|
32131
|
+
childrenRef.current = children;
|
|
32132
|
+
const readShownBranchRef = useRef(null);
|
|
32133
|
+
if (!readShownBranchRef.current) {
|
|
32134
|
+
readShownBranchRef.current = () => {
|
|
32135
|
+
if (routeRenderFrozen) {
|
|
32136
|
+
return shownBranchRef.current;
|
|
32137
|
+
}
|
|
32138
|
+
return collectBranches(childrenRef.current).activeBranch;
|
|
32139
|
+
};
|
|
32140
|
+
}
|
|
32141
|
+
const parentPage = useContext(RoutePageContext);
|
|
32142
|
+
const content = useContentKeepingLeavingPages(branch, readShownBranchRef.current, parentPage);
|
|
31320
32143
|
|
|
31321
32144
|
// The two things this component knows that nobody outside can find out: the
|
|
31322
32145
|
// pages a named fallback is the absence of — the children it was written
|
|
@@ -31353,7 +32176,7 @@ const RouteContainer = ({
|
|
|
31353
32176
|
// page being left must stay the child it was when a page is added beside it,
|
|
31354
32177
|
// or Preact takes it down and builds it again.
|
|
31355
32178
|
let leavingKeyCount = 0;
|
|
31356
|
-
const useContentKeepingLeavingPages = branch => {
|
|
32179
|
+
const useContentKeepingLeavingPages = (branch, readShownBranch, parentPage) => {
|
|
31357
32180
|
const [, rerender] = useReducer(countRenders, 0);
|
|
31358
32181
|
const shownRef = useRef(null);
|
|
31359
32182
|
const leavingRef = useRef([]);
|
|
@@ -31371,6 +32194,7 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
31371
32194
|
// of reach — the page arriving could not even take the focus — for as long
|
|
31372
32195
|
// as it is open, and one in a page kept while leaving stays open for the
|
|
31373
32196
|
// whole movement. The page is then taken down at once, as without one.
|
|
32197
|
+
shown.page.left = true;
|
|
31374
32198
|
leavingRef.current.push({
|
|
31375
32199
|
vnode: shown.vnode,
|
|
31376
32200
|
page: shown.page,
|
|
@@ -31387,10 +32211,11 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
31387
32211
|
let vnode = null;
|
|
31388
32212
|
if (branch) {
|
|
31389
32213
|
if (!page) {
|
|
31390
|
-
page =
|
|
31391
|
-
leavingSignal: signal(false)
|
|
31392
|
-
};
|
|
32214
|
+
page = createRoutePage(readShownBranch, parentPage);
|
|
31393
32215
|
}
|
|
32216
|
+
// A page carried over follows the branch it is now the page of.
|
|
32217
|
+
page.branchId = branch.id;
|
|
32218
|
+
page.element = element;
|
|
31394
32219
|
vnode = h(RoutePageContext.Provider, {
|
|
31395
32220
|
key,
|
|
31396
32221
|
value: page
|
|
@@ -31457,6 +32282,33 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
31457
32282
|
return children;
|
|
31458
32283
|
};
|
|
31459
32284
|
const countRenders = count => count + 1;
|
|
32285
|
+
// The page a container renders, as RoutePageContext hands it over (see
|
|
32286
|
+
// route_page_context.js). Shown while the container would pick its branch, or
|
|
32287
|
+
// a branch rendering the same element (it is then carried over), and while the
|
|
32288
|
+
// page it sits in is shown itself. Never again once kept while leaving: coming
|
|
32289
|
+
// back to its route mounts the page anew.
|
|
32290
|
+
const createRoutePage = (readShownBranch, parentPage) => {
|
|
32291
|
+
const page = {
|
|
32292
|
+
branchId: null,
|
|
32293
|
+
element: undefined,
|
|
32294
|
+
left: false,
|
|
32295
|
+
leavingSignal: signal(false),
|
|
32296
|
+
isShown: () => {
|
|
32297
|
+
if (page.left) {
|
|
32298
|
+
return false;
|
|
32299
|
+
}
|
|
32300
|
+
if (parentPage && !parentPage.isShown()) {
|
|
32301
|
+
return false;
|
|
32302
|
+
}
|
|
32303
|
+
const shownBranch = untracked(readShownBranch);
|
|
32304
|
+
if (!shownBranch) {
|
|
32305
|
+
return false;
|
|
32306
|
+
}
|
|
32307
|
+
return shownBranch.id === page.branchId || branchElement(shownBranch) === page.element;
|
|
32308
|
+
}
|
|
32309
|
+
};
|
|
32310
|
+
return page;
|
|
32311
|
+
};
|
|
31460
32312
|
// What Preact compares when a container changes page, below the <Route> it
|
|
31461
32313
|
// renders either way: the element the page is rendered with — a layout, for a
|
|
31462
32314
|
// container branch.
|
|
@@ -38052,504 +38904,6 @@ const flushSyncRendering = (fn) => {
|
|
|
38052
38904
|
}
|
|
38053
38905
|
};
|
|
38054
38906
|
|
|
38055
|
-
/**
|
|
38056
|
-
* Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
|
|
38057
|
-
* (non-top-layer) renderers — operate on a plain DOM element, no knowledge
|
|
38058
|
-
* of which of the two owns it.
|
|
38059
|
-
*/
|
|
38060
|
-
|
|
38061
|
-
|
|
38062
|
-
/**
|
|
38063
|
-
* Whether a visibleRectEffect delivery is one that can have taken height away
|
|
38064
|
-
* from a popup, and so pushed whatever holds focus out of sight:
|
|
38065
|
-
* - "resize": the window/visual viewport settled — which is also how the
|
|
38066
|
-
* on-screen keyboard arrives, overlay or not (window_size.js);
|
|
38067
|
-
* - ELEMENT_SIZE_CHANGE: the popup's own box measured different;
|
|
38068
|
-
* - "focusin": the focus-settled re-measure, for the room that changes with
|
|
38069
|
-
* nothing announcing it (subscribeFocusSettled in window_size.js).
|
|
38070
|
-
*
|
|
38071
|
-
* Everything else is a scroll of one kind or another, where nothing got
|
|
38072
|
-
* smaller and scrolling the focused element back would fight the very gesture
|
|
38073
|
-
* that fired it.
|
|
38074
|
-
*/
|
|
38075
|
-
const mayHaveHiddenFocus = (event) => {
|
|
38076
|
-
const type = event?.type;
|
|
38077
|
-
return (
|
|
38078
|
-
type === "resize" || type === ELEMENT_SIZE_CHANGE || type === "focusin"
|
|
38079
|
-
);
|
|
38080
|
-
};
|
|
38081
|
-
|
|
38082
|
-
const scrollportHeightMap = new WeakMap();
|
|
38083
|
-
/**
|
|
38084
|
-
* Scrolls whatever holds focus inside `popupEl` back into view, if the popup
|
|
38085
|
-
* getting shorter has pushed it out.
|
|
38086
|
-
*
|
|
38087
|
-
* The case this exists for: a field low in the scrolling body of a popup that
|
|
38088
|
-
* also has a footer (box.jsx — with a body, the body is the only thing that
|
|
38089
|
-
* scrolls and the footer is a sibling sitting right under it). Focusing the
|
|
38090
|
-
* field makes the browser scroll it into view, which it does against the
|
|
38091
|
-
* popup's height AT THAT MOMENT; the on-screen keyboard then opens and takes
|
|
38092
|
-
* that height away. The body shrinks, its scrollTop does not move, so the
|
|
38093
|
-
* content slides down relative to the shorter scrollport and the field ends up
|
|
38094
|
-
* past its bottom edge — visually, swallowed by the footer. The browser does
|
|
38095
|
-
* not redo a scroll-into-view it already answered, so this does.
|
|
38096
|
-
*
|
|
38097
|
-
* Only what the shrink itself hid, though: with the keyboard up the user reads
|
|
38098
|
-
* the rest of the popup by scrolling the field away — to reach the submit under
|
|
38099
|
-
* it, typically — and the room keeps changing while they do (a keyboard settling
|
|
38100
|
-
* in two steps, a suggestion strip, a browser bar). Answering each of those by
|
|
38101
|
-
* scrolling the field back takes the popup away from wherever they had just
|
|
38102
|
-
* scrolled it, over and over: what they were reading cannot be reached at all
|
|
38103
|
-
* without blurring the field first, and the popup reads as unscrollable. So the
|
|
38104
|
-
* field is brought back only when it was in view before the room shrank, which
|
|
38105
|
-
* one remembered number answers: a resize moves neither scrollTop nor the
|
|
38106
|
-
* element's offset inside the scrolled content, so measuring against the height
|
|
38107
|
-
* the scrollport HAD is measuring the state before the change.
|
|
38108
|
-
*
|
|
38109
|
-
* Scoped to the field's own scroll container (never the page): a popup traps
|
|
38110
|
-
* scrolling precisely so the document underneath cannot move, and a plain
|
|
38111
|
-
* scrollIntoView walks past a container whose scrollbar isn't visible — see
|
|
38112
|
-
* scrollIntoViewScoped's own doc.
|
|
38113
|
-
*
|
|
38114
|
-
* "nearest": the smallest scroll that makes it visible, and none at all when it
|
|
38115
|
-
* already is. Where it lands is the container's own business — a navi scroller
|
|
38116
|
-
* keeps a band free at its edges so a field never comes back glued to one (see
|
|
38117
|
-
* scroll-padding in box.jsx).
|
|
38118
|
-
*/
|
|
38119
|
-
const keepFocusedElementVisible = (popupEl) => {
|
|
38120
|
-
const { activeElement } = document;
|
|
38121
|
-
if (!activeElement || activeElement === popupEl) {
|
|
38122
|
-
return;
|
|
38123
|
-
}
|
|
38124
|
-
if (!popupEl.contains(activeElement)) {
|
|
38125
|
-
return;
|
|
38126
|
-
}
|
|
38127
|
-
const scrollContainer = getScrollContainer(activeElement);
|
|
38128
|
-
if (!scrollContainer || !popupEl.contains(scrollContainer)) {
|
|
38129
|
-
// What scrolls the field is outside the popup, which means the page: a
|
|
38130
|
-
// popup holds it still on purpose (trapScrollInside), so there is nothing
|
|
38131
|
-
// here to scroll back.
|
|
38132
|
-
return;
|
|
38133
|
-
}
|
|
38134
|
-
const scrollportHeight = scrollContainer.clientHeight;
|
|
38135
|
-
const scrollportHeightBefore = scrollportHeightMap.get(scrollContainer);
|
|
38136
|
-
scrollportHeightMap.set(scrollContainer, scrollportHeight);
|
|
38137
|
-
if (scrollportHeightBefore !== undefined) {
|
|
38138
|
-
if (scrollportHeight >= scrollportHeightBefore) {
|
|
38139
|
-
// Nothing was taken away, so nothing was hidden by this.
|
|
38140
|
-
return;
|
|
38141
|
-
}
|
|
38142
|
-
if (
|
|
38143
|
-
!isVisibleInScrollport(
|
|
38144
|
-
activeElement,
|
|
38145
|
-
scrollContainer,
|
|
38146
|
-
scrollportHeightBefore,
|
|
38147
|
-
)
|
|
38148
|
-
) {
|
|
38149
|
-
return;
|
|
38150
|
-
}
|
|
38151
|
-
}
|
|
38152
|
-
scrollIntoViewScoped(activeElement, {
|
|
38153
|
-
container: scrollContainer,
|
|
38154
|
-
block: "nearest",
|
|
38155
|
-
});
|
|
38156
|
-
};
|
|
38157
|
-
|
|
38158
|
-
// Whether any part of `el` was showing in `container` back when its scrollport
|
|
38159
|
-
// was `scrollportHeight` tall. Both boxes are read now: the container may have
|
|
38160
|
-
// moved as well as shrunk (a centered dialog re-centers itself), and the
|
|
38161
|
-
// difference between the two tops is what that move leaves alone.
|
|
38162
|
-
const isVisibleInScrollport = (el, container, scrollportHeight) => {
|
|
38163
|
-
const elRect = el.getBoundingClientRect();
|
|
38164
|
-
const containerRect = container.getBoundingClientRect();
|
|
38165
|
-
const elTop = elRect.top - containerRect.top + container.scrollTop;
|
|
38166
|
-
const elBottom = elTop + elRect.height;
|
|
38167
|
-
const scrollportTop = container.scrollTop;
|
|
38168
|
-
const scrollportBottom = scrollportTop + scrollportHeight;
|
|
38169
|
-
return elBottom > scrollportTop && elTop < scrollportBottom;
|
|
38170
|
-
};
|
|
38171
|
-
|
|
38172
|
-
/**
|
|
38173
|
-
* Calls `onSettled` once `el`'s current CSS transition is over — via
|
|
38174
|
-
* `transitionend`, with a safety `setTimeout` fallback matching the longest
|
|
38175
|
-
* `transition-duration`, in case nothing actually transitions or an event is
|
|
38176
|
-
* missed.
|
|
38177
|
-
*
|
|
38178
|
-
* Returns a "cancel" function, so a caller whose instance has been superseded
|
|
38179
|
-
* (a fresh open/close about to set its own state) can keep this stale one from
|
|
38180
|
-
* firing later. Cancelling only stops `onSettled`: undoing whatever the caller
|
|
38181
|
-
* did up front is that fresh call's business, not this one's.
|
|
38182
|
-
*/
|
|
38183
|
-
const whenTransitionSettles = (el, onSettled) => {
|
|
38184
|
-
let settled = false;
|
|
38185
|
-
const onTransitionEnd = (transitionEvent) => {
|
|
38186
|
-
if (transitionEvent.target === el) {
|
|
38187
|
-
finish();
|
|
38188
|
-
}
|
|
38189
|
-
};
|
|
38190
|
-
const stopWatching = () => {
|
|
38191
|
-
settled = true;
|
|
38192
|
-
el.removeEventListener("transitionend", onTransitionEnd);
|
|
38193
|
-
clearTimeout(safetyTimeoutId);
|
|
38194
|
-
};
|
|
38195
|
-
const finish = () => {
|
|
38196
|
-
if (settled) {
|
|
38197
|
-
return;
|
|
38198
|
-
}
|
|
38199
|
-
stopWatching();
|
|
38200
|
-
onSettled();
|
|
38201
|
-
};
|
|
38202
|
-
el.addEventListener("transitionend", onTransitionEnd);
|
|
38203
|
-
const durationsInSeconds = getComputedStyle(el)
|
|
38204
|
-
.transitionDuration.split(",")
|
|
38205
|
-
.map((value) => parseFloat(value) || 0);
|
|
38206
|
-
const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
|
|
38207
|
-
const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
|
|
38208
|
-
return () => {
|
|
38209
|
-
if (settled) {
|
|
38210
|
-
return;
|
|
38211
|
-
}
|
|
38212
|
-
stopWatching();
|
|
38213
|
-
};
|
|
38214
|
-
};
|
|
38215
|
-
|
|
38216
|
-
/**
|
|
38217
|
-
* Drops the document's text selection when it lives inside `el`, leaving a
|
|
38218
|
-
* selection made elsewhere on the page alone.
|
|
38219
|
-
*
|
|
38220
|
-
* A popup being closed takes its content with it, and a selection is a claim
|
|
38221
|
-
* on content the user can still act on: kept, it would outlive the surface it
|
|
38222
|
-
* was made on — painted on the box for the length of its exit transition, with
|
|
38223
|
-
* the handles and the copy toolbar a phone draws from the live selection
|
|
38224
|
-
* hanging over something that is going away — and reappear with the box on
|
|
38225
|
-
* the next opening. Dropping the selection is the only way to remove that
|
|
38226
|
-
* chrome: a `user-select: none` on the closing box hides the highlight in some
|
|
38227
|
-
* browsers only, and the handles are the browser's own, drawn from the
|
|
38228
|
-
* selection object rather than from any style.
|
|
38229
|
-
*/
|
|
38230
|
-
const clearTextSelectionInside = (el) => {
|
|
38231
|
-
const selection = window.getSelection();
|
|
38232
|
-
if (!selection || selection.rangeCount === 0) {
|
|
38233
|
-
return;
|
|
38234
|
-
}
|
|
38235
|
-
const range = selection.getRangeAt(0);
|
|
38236
|
-
if (!el.contains(range.commonAncestorContainer)) {
|
|
38237
|
-
return;
|
|
38238
|
-
}
|
|
38239
|
-
selection.removeAllRanges();
|
|
38240
|
-
};
|
|
38241
|
-
|
|
38242
|
-
/**
|
|
38243
|
-
* Disables pointer-events on `el` until its current CSS transition settles —
|
|
38244
|
-
* avoids the cursor changing/something becoming clickable while the popup is
|
|
38245
|
-
* still visually moving into or out of place.
|
|
38246
|
-
*
|
|
38247
|
-
* Returns whenTransitionSettles' own "cancel" function: it doesn't restore
|
|
38248
|
-
* pointer-events, since a fresh call for the next open/close is about to set
|
|
38249
|
-
* its own state.
|
|
38250
|
-
*/
|
|
38251
|
-
const suppressPointerEventsDuringTransition = (el) => {
|
|
38252
|
-
el.style.pointerEvents = "none";
|
|
38253
|
-
return whenTransitionSettles(el, () => {
|
|
38254
|
-
el.style.pointerEvents = "";
|
|
38255
|
-
});
|
|
38256
|
-
};
|
|
38257
|
-
|
|
38258
|
-
/**
|
|
38259
|
-
* Hides the backdrop, deferring until the browser's matching "click" fires
|
|
38260
|
-
* when `closeEvent` was triggered by a mousedown (see popover.jsx's top
|
|
38261
|
-
* comment for why) — same capture-phase-on-document pattern as
|
|
38262
|
-
* armSuppressNextOpenRequest in open_controller.js, which a plain timeout
|
|
38263
|
-
* can't safely replace: mouseup (and the click that follows it) can land an
|
|
38264
|
-
* arbitrarily long time after mousedown (the user is still holding the
|
|
38265
|
-
* button down), so a short timeout can fire first and hide the backdrop
|
|
38266
|
-
* before its own click ever arrives. A capture-phase listener on document
|
|
38267
|
-
* fires for every click regardless of what any bubble-phase handler does
|
|
38268
|
-
* downstream, so no fallback timer is needed.
|
|
38269
|
-
*
|
|
38270
|
-
* `hide` is the caller's own way to actually hide the backdrop
|
|
38271
|
-
* (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
|
|
38272
|
-
* for a plain div) — this helper only owns the mousedown/click timing.
|
|
38273
|
-
*
|
|
38274
|
-
* Returns a disarm function (or undefined if hidden immediately), so a
|
|
38275
|
-
* fresh open can cancel a pending hide it's about to make redundant.
|
|
38276
|
-
*/
|
|
38277
|
-
const armPointerDownOutsideClose = (closeEvent, hide) => {
|
|
38278
|
-
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
38279
|
-
if (!mousedownEvent) {
|
|
38280
|
-
hide();
|
|
38281
|
-
return undefined;
|
|
38282
|
-
}
|
|
38283
|
-
const onClick = () => {
|
|
38284
|
-
document.removeEventListener("click", onClick, { capture: true });
|
|
38285
|
-
hide();
|
|
38286
|
-
};
|
|
38287
|
-
document.addEventListener("click", onClick, { capture: true });
|
|
38288
|
-
return () => {
|
|
38289
|
-
document.removeEventListener("click", onClick, { capture: true });
|
|
38290
|
-
};
|
|
38291
|
-
};
|
|
38292
|
-
|
|
38293
|
-
/**
|
|
38294
|
-
* Maps a positionArea y/x pair to a concrete `navi-animation` value (a
|
|
38295
|
-
* `prefix` plus a direction word), or `null` if both axes overlap the anchor
|
|
38296
|
-
* (no direction at all — that's `resolvedAnimationKind === "scaling"`
|
|
38297
|
-
* territory instead, see resolveAutoAnimationKind below).
|
|
38298
|
-
*
|
|
38299
|
-
* `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
|
|
38300
|
-
* when docked) keeps the word as the compass direction the popup comes
|
|
38301
|
-
* from: placed "top" (a point/corner), it slides in from the top.
|
|
38302
|
-
* `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
|
|
38303
|
-
* direction instead, the opposite compass point: placed "top" of the
|
|
38304
|
-
* anchor, it moves/grows up, away from the anchor (which sits below it).
|
|
38305
|
-
*
|
|
38306
|
-
* "inset-*"/"center" contribute no direction on their axis either way.
|
|
38307
|
-
*/
|
|
38308
|
-
const resolveDirectionValue = (y, x, { prefix }) => {
|
|
38309
|
-
const yWord =
|
|
38310
|
-
y === "top"
|
|
38311
|
-
? prefix === "expand"
|
|
38312
|
-
? "up"
|
|
38313
|
-
: "top"
|
|
38314
|
-
: y === "bottom"
|
|
38315
|
-
? prefix === "expand"
|
|
38316
|
-
? "down"
|
|
38317
|
-
: "bottom"
|
|
38318
|
-
: null;
|
|
38319
|
-
const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
|
|
38320
|
-
if (!yWord && !xWord) {
|
|
38321
|
-
return null;
|
|
38322
|
-
}
|
|
38323
|
-
return yWord && xWord
|
|
38324
|
-
? `${prefix}-${yWord}-${xWord}`
|
|
38325
|
-
: `${prefix}-${yWord || xWord}`;
|
|
38326
|
-
};
|
|
38327
|
-
|
|
38328
|
-
/**
|
|
38329
|
-
* Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
|
|
38330
|
-
* — picked for any real anchor, or for a point/corner placed dead-center
|
|
38331
|
-
* (both positionArea axes overlapping — there's no sensible direction to
|
|
38332
|
-
* slide from in that case). "sliding" otherwise. `anchor` is `undefined`
|
|
38333
|
-
* for any no-anchor/docked case (Dialog always, Popover's own custom
|
|
38334
|
-
* renderer when there's no real anchor), so this collapses to "scaling"
|
|
38335
|
-
* there only for the dead-center case, "sliding" otherwise. The two
|
|
38336
|
-
* "overlapping" booleans below describe the *positionArea* itself (a bare
|
|
38337
|
-
* word vs. "inset-"/"center"), not anything about the anchor — they'd
|
|
38338
|
-
* mean exactly the same thing even with no anchor at all, since it's the
|
|
38339
|
-
* position strategy, not the anchor, that decides whether there's a
|
|
38340
|
-
* direction to slide from.
|
|
38341
|
-
*/
|
|
38342
|
-
const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
|
|
38343
|
-
const yIsOverlapping =
|
|
38344
|
-
parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
|
|
38345
|
-
const xIsOverlapping =
|
|
38346
|
-
parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
|
|
38347
|
-
return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
|
|
38348
|
-
};
|
|
38349
|
-
|
|
38350
|
-
/**
|
|
38351
|
-
* A popup reads "outside" from its own border box: what a press lands on
|
|
38352
|
-
* decides nothing on its own (a genuine backdrop press and a press on the
|
|
38353
|
-
* popup's padding both report the popup element as their target, there being
|
|
38354
|
-
* no real ::backdrop node to be one), so the rectangle is what tells them
|
|
38355
|
-
* apart. That holds as long as the box and what the popup paints are the same
|
|
38356
|
-
* thing — which stops being true for a popup with no surface of its own
|
|
38357
|
-
* (`backgroundColor="transparent"`, no shadow, no padding): what the eye reads
|
|
38358
|
-
* as backdrop is then inside the box, and a press there stays a press on the
|
|
38359
|
-
* popup.
|
|
38360
|
-
*
|
|
38361
|
-
* `data-navi-popup-outside` is how a caller says which of its own boxes are
|
|
38362
|
-
* not the surface. It is opt-in because navi cannot infer it — a background
|
|
38363
|
-
* can come from anywhere — while the caller who made the popup see-through
|
|
38364
|
-
* knows exactly which box is decoration and which is paper.
|
|
38365
|
-
*
|
|
38366
|
-
* The marker answers for the element it is on, never for its descendants: a
|
|
38367
|
-
* box painted inside a marked one is still surface, so a row can be marked and
|
|
38368
|
-
* its empty halves read as backdrop while presses on the controls it holds do
|
|
38369
|
-
* not dismiss anything. A descendant with `pointer-events: none` never becomes
|
|
38370
|
-
* a press target at all, so the marked box answers in its place — while the
|
|
38371
|
-
* same declaration on the marked box itself takes it out of hit-testing and
|
|
38372
|
-
* makes the marker unreachable (see warnAboutUnreachableOutsideRegions).
|
|
38373
|
-
*/
|
|
38374
|
-
const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
|
|
38375
|
-
|
|
38376
|
-
/**
|
|
38377
|
-
* `data-navi-popup-inside` is the other direction: a box of the PAGE the
|
|
38378
|
-
* caller says belongs to a popup, so a press on it is not outside that popup
|
|
38379
|
-
* even though it lies beyond the border box — a card whose press is what
|
|
38380
|
-
* fills the one panel of a board. Its value names the popup by `id`, the way
|
|
38381
|
-
* `commandfor` does (several ids, space separated), because the exemption is
|
|
38382
|
-
* one popup's: a menu open beside the board still closes on the same press.
|
|
38383
|
-
*
|
|
38384
|
-
* Unlike the outside marker it answers for its whole subtree: the outside
|
|
38385
|
-
* marker carves free space out of a surface whose controls stay surface, this
|
|
38386
|
-
* one adds a whole thing — the card, its count, its buttons — to the popup's
|
|
38387
|
-
* own ground, and nothing in it is a dismissal.
|
|
38388
|
-
*/
|
|
38389
|
-
const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
|
|
38390
|
-
|
|
38391
|
-
/**
|
|
38392
|
-
* What a press landing on a region the caller declared as not-its-surface
|
|
38393
|
-
* does: exactly what the same press on the backdrop would do.
|
|
38394
|
-
*
|
|
38395
|
-
* Lives on the popup's own content element rather than in the backdrop's
|
|
38396
|
-
* handler, for both renderers: the backdrop is a sibling behind the popup
|
|
38397
|
-
* (never an ancestor), so a press inside the popup's box never reaches it.
|
|
38398
|
-
*/
|
|
38399
|
-
const handlePressOnOutsideRegion = (
|
|
38400
|
-
mouseDownEvent,
|
|
38401
|
-
{ popupEl, openController, pressOutside },
|
|
38402
|
-
) => {
|
|
38403
|
-
if (mouseDownEvent.button !== 0) {
|
|
38404
|
-
return;
|
|
38405
|
-
}
|
|
38406
|
-
const { target } = mouseDownEvent;
|
|
38407
|
-
if (!target.hasAttribute(OUTSIDE_REGION_ATTRIBUTE)) {
|
|
38408
|
-
return;
|
|
38409
|
-
}
|
|
38410
|
-
if (openedDuringThisPress(openController)) {
|
|
38411
|
-
// The release of the press that opened it is not somebody dismissing it
|
|
38412
|
-
// (see openedDuringThisPress).
|
|
38413
|
-
return;
|
|
38414
|
-
}
|
|
38415
|
-
// A popup opens inside its opener's own subtree, so a press on a region
|
|
38416
|
-
// belonging to a popup nested in this one bubbles through here too — and it
|
|
38417
|
-
// is a press on what is in front, not on this popup's own decoration.
|
|
38418
|
-
if (
|
|
38419
|
-
target.closest(`[navi-control="dialog"], [navi-control="popover"]`) !==
|
|
38420
|
-
popupEl
|
|
38421
|
-
) {
|
|
38422
|
-
return;
|
|
38423
|
-
}
|
|
38424
|
-
if (pressOutside === "capture") {
|
|
38425
|
-
mouseDownEvent.preventDefault();
|
|
38426
|
-
return;
|
|
38427
|
-
}
|
|
38428
|
-
if (pressOutside === "close" || pressOutside === "cancel") {
|
|
38429
|
-
openController.requestClose(mouseDownEvent, {
|
|
38430
|
-
isCancel: pressOutside === "cancel",
|
|
38431
|
-
});
|
|
38432
|
-
}
|
|
38433
|
-
};
|
|
38434
|
-
|
|
38435
|
-
/**
|
|
38436
|
-
* Hears an outside press from the document itself, for a popup with no
|
|
38437
|
-
* backdrop of its own to catch one: a modal `<dialog>` (whose native
|
|
38438
|
-
* `::backdrop` dispatches nothing a handler can be put on) and any popup the
|
|
38439
|
-
* caller asked to leave the page reachable (`backdrop={false}`).
|
|
38440
|
-
*
|
|
38441
|
-
* The press is read and never taken: no `preventDefault`, no
|
|
38442
|
-
* `stopPropagation`, and the listener is on `document` rather than on
|
|
38443
|
-
* anything covering the page — so what the press landed on hears it too, in
|
|
38444
|
-
* the same gesture, and the popup only takes note. That is the whole point of
|
|
38445
|
-
* a popup with no wall: a press outside is a press on the page, and the page
|
|
38446
|
-
* answers it. The one popup where that cannot hold is the modal dialog, where
|
|
38447
|
-
* the browser has already made everything behind inert before this runs.
|
|
38448
|
-
*
|
|
38449
|
-
* `pressEventType` says which event IS the press, and the two callers want
|
|
38450
|
-
* opposite ones:
|
|
38451
|
-
*
|
|
38452
|
-
* - `"pointerdown"` for a popup with no wall: a page with no wall over it is
|
|
38453
|
-
* a page whose own elements arbitrate their presses, and cancelling a
|
|
38454
|
-
* `pointerdown` — what a drag source and a control keeping the focus where
|
|
38455
|
-
* it is both do — suppresses every compatibility mouse event that would have
|
|
38456
|
-
* followed it. The press happens, reaches its target, and no `mousedown` is
|
|
38457
|
-
* ever dispatched for it. Reading the pointer event is reading the press
|
|
38458
|
-
* itself, whatever anything does with it afterwards, and it is the same
|
|
38459
|
-
* event the drag and surface detectors read.
|
|
38460
|
-
* - `"mousedown"` for the modal dialog, whose native `::backdrop` exists to
|
|
38461
|
-
* spend the press. The page's controls act on `mousedown`, and on a touch
|
|
38462
|
-
* screen that is not the same moment as `pointerdown`: the compatibility
|
|
38463
|
-
* mouse events are synthesized at `touchend` and hit-tested again then.
|
|
38464
|
-
* Closing on `pointerdown` takes the `::backdrop` away before that second
|
|
38465
|
-
* hit-test, so the `mousedown` lands on whatever the page holds under the
|
|
38466
|
-
* finger, and one tap both dismisses the dialog and presses the page. Read
|
|
38467
|
-
* on `mousedown`, the wall is still up when the press the page acts on is
|
|
38468
|
-
* aimed, and it catches it. Nothing behind a modal wall can cancel that
|
|
38469
|
-
* press, since nothing behind it hears the `pointerdown` at all.
|
|
38470
|
-
*
|
|
38471
|
-
* Capture phase, so a handler downstream that stops propagation cannot keep
|
|
38472
|
-
* the popup open either — the press is still outside whatever is made of it.
|
|
38473
|
-
*
|
|
38474
|
-
* Returns a cleanup that removes the listener.
|
|
38475
|
-
*/
|
|
38476
|
-
const armOutsidePressClose = (
|
|
38477
|
-
popupEl,
|
|
38478
|
-
{ openController, pressOutside, pressEventType = "pointerdown" },
|
|
38479
|
-
) => {
|
|
38480
|
-
const onDocumentPointerDown = (pointerDownEvent) => {
|
|
38481
|
-
if (pointerDownEvent.button !== 0) {
|
|
38482
|
-
return;
|
|
38483
|
-
}
|
|
38484
|
-
if (openedDuringThisPress(openController)) {
|
|
38485
|
-
// The release of the press that opened it is not somebody dismissing it
|
|
38486
|
-
// (see openedDuringThisPress).
|
|
38487
|
-
return;
|
|
38488
|
-
}
|
|
38489
|
-
// The press landed inside another popup: that is a press on what is in
|
|
38490
|
-
// front, not outside. Asking the target where it lives rather than asking
|
|
38491
|
-
// this popup whether it was pushed — a popup in front does not have to be
|
|
38492
|
-
// one this popup knows about. In front means neither inside nor around: a
|
|
38493
|
-
// popup nested in this one falls to the containment check below as the
|
|
38494
|
-
// inside press it is, and one this popup opened INSIDE (a bubble rendered
|
|
38495
|
-
// in a sheet's content) is around it, so what that popup holds is outside
|
|
38496
|
-
// this one exactly like the rest of the page.
|
|
38497
|
-
const popupUnderPointer = pointerDownEvent.target.closest?.(
|
|
38498
|
-
`[navi-control="dialog"], [navi-control="popover"]`,
|
|
38499
|
-
);
|
|
38500
|
-
if (
|
|
38501
|
-
popupUnderPointer &&
|
|
38502
|
-
popupUnderPointer !== popupEl &&
|
|
38503
|
-
!popupEl.contains(popupUnderPointer) &&
|
|
38504
|
-
!popupUnderPointer.contains(popupEl)
|
|
38505
|
-
) {
|
|
38506
|
-
return;
|
|
38507
|
-
}
|
|
38508
|
-
if (
|
|
38509
|
-
popupEl.id &&
|
|
38510
|
-
pointerDownEvent.target.closest?.(
|
|
38511
|
-
`[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupEl.id)}"]`,
|
|
38512
|
-
)
|
|
38513
|
-
) {
|
|
38514
|
-
return;
|
|
38515
|
-
}
|
|
38516
|
-
// Real DOM containment wins over the rectangle below — an element
|
|
38517
|
-
// genuinely inside the popup (`overflow: visible`, a negative margin, an
|
|
38518
|
-
// absolutely-positioned child) can be painted outside its border box, and
|
|
38519
|
-
// a press there is not outside just because its coordinates are. The
|
|
38520
|
-
// popup element itself is excluded (contains() answers true for it): a
|
|
38521
|
-
// press on a modal dialog's native backdrop reports the dialog as its
|
|
38522
|
-
// target, there being no `::backdrop` node to be one, so treating that as
|
|
38523
|
-
// contained would keep the rectangle from ever running.
|
|
38524
|
-
if (
|
|
38525
|
-
pointerDownEvent.target !== popupEl &&
|
|
38526
|
-
popupEl.contains(pointerDownEvent.target)
|
|
38527
|
-
) {
|
|
38528
|
-
return;
|
|
38529
|
-
}
|
|
38530
|
-
const rect = popupEl.getBoundingClientRect();
|
|
38531
|
-
const isOutside =
|
|
38532
|
-
pointerDownEvent.clientX < rect.left ||
|
|
38533
|
-
pointerDownEvent.clientX > rect.right ||
|
|
38534
|
-
pointerDownEvent.clientY < rect.top ||
|
|
38535
|
-
pointerDownEvent.clientY > rect.bottom;
|
|
38536
|
-
if (!isOutside) {
|
|
38537
|
-
return;
|
|
38538
|
-
}
|
|
38539
|
-
openController.requestClose(pointerDownEvent, {
|
|
38540
|
-
isCancel: pressOutside === "cancel",
|
|
38541
|
-
});
|
|
38542
|
-
};
|
|
38543
|
-
document.addEventListener(pressEventType, onDocumentPointerDown, {
|
|
38544
|
-
capture: true,
|
|
38545
|
-
});
|
|
38546
|
-
return () => {
|
|
38547
|
-
document.removeEventListener(pressEventType, onDocumentPointerDown, {
|
|
38548
|
-
capture: true,
|
|
38549
|
-
});
|
|
38550
|
-
};
|
|
38551
|
-
};
|
|
38552
|
-
|
|
38553
38907
|
/**
|
|
38554
38908
|
* When a popup builds what it holds, and when it throws it away.
|
|
38555
38909
|
*
|
|
@@ -65539,6 +65893,13 @@ const css$D = /* css */`
|
|
|
65539
65893
|
* is a `Popover`-level answer only for `"close"`/`"cancel"`; `"capture"` has
|
|
65540
65894
|
* nothing left to absorb without a wall, and `"ignore"` renders no wall anyway.
|
|
65541
65895
|
* See docs/popup_backdrop.md.
|
|
65896
|
+
* @param {string} [props.group] - The popover's peers: the triggers carrying
|
|
65897
|
+
* `data-navi-popup-group` with this name. A press on one, while the popover
|
|
65898
|
+
* is open, closes it and opens the peer's popup in that same press — a
|
|
65899
|
+
* menu bar, where pressing another menu switches to it. The wall still
|
|
65900
|
+
* takes the press (the page behind stays out of reach, hover included) and
|
|
65901
|
+
* hands its click to the peer it covered. Without a wall there is nothing
|
|
65902
|
+
* to ask: the press reaches the peer on its own.
|
|
65542
65903
|
* @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="ignore"]
|
|
65543
65904
|
* - `"ignore"` (default): no backdrop at all, outside presses pass straight
|
|
65544
65905
|
* through. `"close"` closes the popover on an outside press, `"cancel"`
|
|
@@ -65913,6 +66274,7 @@ const usePopoverProps = props => {
|
|
|
65913
66274
|
// which is a sibling element (see this file's top comment).
|
|
65914
66275
|
backdropColor,
|
|
65915
66276
|
backdropFilter,
|
|
66277
|
+
group,
|
|
65916
66278
|
scrollCapture,
|
|
65917
66279
|
focusCapture,
|
|
65918
66280
|
// "auto" (default) → the popover follows its content. "frozen" → measured
|
|
@@ -66558,13 +66920,21 @@ const usePopoverProps = props => {
|
|
|
66558
66920
|
}
|
|
66559
66921
|
if (backdropEl) {
|
|
66560
66922
|
backdropEl.setAttribute("aria-expanded", "false");
|
|
66561
|
-
disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent,
|
|
66923
|
+
disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent, clickEvent => {
|
|
66562
66924
|
// Set regardless of isTopLayer — see openEffect's own identical
|
|
66563
66925
|
// comment for why hidePopover() alone isn't reliably sufficient.
|
|
66564
66926
|
backdropEl.setAttribute("navi-hidden", "");
|
|
66565
66927
|
if (isTopLayer) {
|
|
66566
66928
|
backdropEl.hidePopover();
|
|
66567
66929
|
}
|
|
66930
|
+
if (group && clickEvent) {
|
|
66931
|
+
handWallClickToPeer(clickEvent, {
|
|
66932
|
+
wallEl: backdropEl,
|
|
66933
|
+
popupEl: popoverEl,
|
|
66934
|
+
anchorEl: anchorElement,
|
|
66935
|
+
group
|
|
66936
|
+
});
|
|
66937
|
+
}
|
|
66568
66938
|
});
|
|
66569
66939
|
}
|
|
66570
66940
|
restoreFocus?.(closeEvent);
|
|
@@ -66986,6 +67356,10 @@ const Popup = props => {
|
|
|
66986
67356
|
scrollCapture,
|
|
66987
67357
|
positionAreaFixed,
|
|
66988
67358
|
positionAreaWhenAnchorIsInvalid,
|
|
67359
|
+
// A top-layer dialog's wall is its native ::backdrop, which goes with the
|
|
67360
|
+
// dialog on the press that closes it: no click is left on it to hand to a
|
|
67361
|
+
// peer (see handWallClickToPeer).
|
|
67362
|
+
group,
|
|
66989
67363
|
// Dialog-only, destructured out for the same reason: forwarded below only
|
|
66990
67364
|
// in the dialog branch, so they never reach the popover element.
|
|
66991
67365
|
dockedOnSmallTouchScreen,
|
|
@@ -67035,6 +67409,7 @@ const Popup = props => {
|
|
|
67035
67409
|
positionAreaWhenAnchorIsInvalid: positionAreaWhenAnchorIsInvalid,
|
|
67036
67410
|
scrollCapture: scrollCapture === "popover" || scrollCapture,
|
|
67037
67411
|
positionAreaFixed: positionAreaFixed,
|
|
67412
|
+
group: group,
|
|
67038
67413
|
className: withPropsClassName("navi_popup", className),
|
|
67039
67414
|
expand: expand,
|
|
67040
67415
|
expandX: expandX,
|
|
@@ -67938,13 +68313,18 @@ const PickerContentInsidePopup = props => {
|
|
|
67938
68313
|
calloutIcon,
|
|
67939
68314
|
calloutCloseButton,
|
|
67940
68315
|
calloutCloseByScroll,
|
|
68316
|
+
popupGroup,
|
|
67941
68317
|
...rest
|
|
67942
68318
|
} = props;
|
|
67943
68319
|
const isPopover = mode === "popover";
|
|
67944
68320
|
const isCallout = mode === "callout";
|
|
67945
68321
|
return jsx(Next, {
|
|
67946
68322
|
"aria-haspopup": isPopover ? "listbox" : "dialog",
|
|
67947
|
-
"navi-popover-mode": isPopover ? popoverMode : undefined
|
|
68323
|
+
"navi-popover-mode": isPopover ? popoverMode : undefined
|
|
68324
|
+
// On the trigger, whatever the mode: a peer's popup reads it off the
|
|
68325
|
+
// press that lands here (see `group` in callout.js).
|
|
68326
|
+
,
|
|
68327
|
+
"data-navi-popup-group": popupGroup,
|
|
67948
68328
|
...rest,
|
|
67949
68329
|
// On popupProps already (see the picker's popup assembly); they mean
|
|
67950
68330
|
// nothing to the picker element.
|
|
@@ -67980,7 +68360,12 @@ const PickerContentInsidePopup = props => {
|
|
|
67980
68360
|
status: calloutStatus,
|
|
67981
68361
|
icon: calloutIcon,
|
|
67982
68362
|
closeButton: calloutCloseButton,
|
|
67983
|
-
closeByScroll: calloutCloseByScroll
|
|
68363
|
+
closeByScroll: calloutCloseByScroll
|
|
68364
|
+
// Left unsaid, the callout's own default: no backdrop. It dims
|
|
68365
|
+
// nothing, so the page looks as pressable as it did before.
|
|
68366
|
+
,
|
|
68367
|
+
backdrop: backdrop,
|
|
68368
|
+
group: popupGroup,
|
|
67984
68369
|
children: jsx(PopupModeContext.Provider, {
|
|
67985
68370
|
value: mode,
|
|
67986
68371
|
children: children
|
|
@@ -68017,6 +68402,7 @@ const PickerContentInsidePopup = props => {
|
|
|
68017
68402
|
backdropVariant: backdropVariant,
|
|
68018
68403
|
backdropColor: backdropColor,
|
|
68019
68404
|
backdropFilter: backdropFilter,
|
|
68405
|
+
group: popupGroup,
|
|
68020
68406
|
focusCapture: isPopover ? focusCapture : undefined,
|
|
68021
68407
|
expand: isPopover ? undefined : dialogExpand,
|
|
68022
68408
|
expandX: isPopover ? undefined : dialogExpandX,
|
|
@@ -68076,6 +68462,8 @@ const PickerCalloutPopup = ({
|
|
|
68076
68462
|
icon,
|
|
68077
68463
|
closeButton,
|
|
68078
68464
|
closeByScroll,
|
|
68465
|
+
backdrop,
|
|
68466
|
+
group,
|
|
68079
68467
|
onnavi_request_open,
|
|
68080
68468
|
onnavi_request_close,
|
|
68081
68469
|
onnavi_request_confirm,
|
|
@@ -68117,6 +68505,8 @@ const PickerCalloutPopup = ({
|
|
|
68117
68505
|
icon,
|
|
68118
68506
|
closeButton,
|
|
68119
68507
|
closeByScroll,
|
|
68508
|
+
backdrop,
|
|
68509
|
+
group,
|
|
68120
68510
|
anchorElement,
|
|
68121
68511
|
// The request, chained to the press that made it: the callout reads the
|
|
68122
68512
|
// mousedown off it to wait for the release before listening for a click
|
|
@@ -70283,7 +70673,13 @@ const ListRowContext = createContext(null);
|
|
|
70283
70673
|
// row takes its place in the collection by slot: the place is then the list's
|
|
70284
70674
|
// to move, and the row's to follow — see list_rows.js.
|
|
70285
70675
|
const ListSlotContext = createContext(null);
|
|
70286
|
-
const css$x = /* css */`@
|
|
70676
|
+
const css$x = /* css */`@property --list-group-label-height {
|
|
70677
|
+
syntax: "<length>";
|
|
70678
|
+
inherits: false;
|
|
70679
|
+
initial-value: 0;
|
|
70680
|
+
}
|
|
70681
|
+
|
|
70682
|
+
@layer navi {
|
|
70287
70683
|
.navi_list_container {
|
|
70288
70684
|
--list-outline-width: 1px;
|
|
70289
70685
|
--list-border-radius: 4px;
|
|
@@ -70682,6 +71078,7 @@ const css$x = /* css */`@layer navi {
|
|
|
70682
71078
|
}
|
|
70683
71079
|
|
|
70684
71080
|
& .navi_list_item_group_list {
|
|
71081
|
+
--list-group-label-height: inherit;
|
|
70685
71082
|
flex-direction: column;
|
|
70686
71083
|
width: 100%;
|
|
70687
71084
|
margin: 0;
|
|
@@ -70689,8 +71086,12 @@ const css$x = /* css */`@layer navi {
|
|
|
70689
71086
|
list-style: none;
|
|
70690
71087
|
display: flex;
|
|
70691
71088
|
|
|
71089
|
+
& > .navi_list_item {
|
|
71090
|
+
--list-group-label-height: inherit;
|
|
71091
|
+
}
|
|
71092
|
+
|
|
70692
71093
|
& .navi_list_item {
|
|
70693
|
-
scroll-margin-top: calc(var(--x-list-scroll-spacing-top) + var(--list-group-label-height
|
|
71094
|
+
scroll-margin-top: calc(var(--x-list-scroll-spacing-top) + var(--list-group-label-height));
|
|
70694
71095
|
}
|
|
70695
71096
|
}
|
|
70696
71097
|
|