@jsenv/navi 0.29.416 → 0.29.418
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 +1595 -1103
- package/dist/dev/jsenv_navi.js.map +19 -17
- package/dist/jsenv_navi.js +1566 -1088
- package/dist/jsenv_navi.js.map +19 -17
- package/docs/AI_INSTRUCTIONS.md +13 -6
- package/docs/drag_interactions.md +297 -401
- package/docs/drag_to_travel.md +157 -754
- package/docs/interactions.md +170 -328
- package/docs/mobile_touch.md +35 -0
- package/docs/resource.md +13 -0
- package/docs/route_transitions.md +35 -7
- package/docs/scroll.md +23 -5
- package/docs/state_binding.md +39 -84
- 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";
|
|
@@ -10826,6 +10826,28 @@ const suspendScrollRecording = () => {
|
|
|
10826
10826
|
};
|
|
10827
10827
|
};
|
|
10828
10828
|
|
|
10829
|
+
// A list scrolling the document that opens on a row puts the document where
|
|
10830
|
+
// that row is, by measuring it (see placeWhereHeld in list.jsx). The offset kept
|
|
10831
|
+
// for the url is pixels of the page as it was drawn — and a list draws a window
|
|
10832
|
+
// of its rows and holds the room of the others with fillers of an estimated
|
|
10833
|
+
// height, so the same pixels now fall on other rows. Put back after the list
|
|
10834
|
+
// has placed itself, they would undo it, in the very picture a route transition
|
|
10835
|
+
// takes. So the list holds the document while it is placing itself, and the
|
|
10836
|
+
// url's offset is not put back meanwhile. Counted: two lists can share one
|
|
10837
|
+
// document.
|
|
10838
|
+
let documentHoldCount = 0;
|
|
10839
|
+
const holdDocumentScroll = () => {
|
|
10840
|
+
documentHoldCount++;
|
|
10841
|
+
let released = false;
|
|
10842
|
+
return () => {
|
|
10843
|
+
if (released) {
|
|
10844
|
+
return;
|
|
10845
|
+
}
|
|
10846
|
+
released = true;
|
|
10847
|
+
documentHoldCount--;
|
|
10848
|
+
};
|
|
10849
|
+
};
|
|
10850
|
+
|
|
10829
10851
|
let installed = false;
|
|
10830
10852
|
const installScrollRestoration = () => {
|
|
10831
10853
|
if (installed) {
|
|
@@ -10860,6 +10882,9 @@ const installScrollRestoration = () => {
|
|
|
10860
10882
|
if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
|
|
10861
10883
|
const stopListening = observeRouteRender(() => {
|
|
10862
10884
|
stopListening();
|
|
10885
|
+
if (documentHoldCount) {
|
|
10886
|
+
return;
|
|
10887
|
+
}
|
|
10863
10888
|
scrollTo(positionOnLoad);
|
|
10864
10889
|
});
|
|
10865
10890
|
}
|
|
@@ -10874,6 +10899,11 @@ const restoreScrollPosition = (url) => {
|
|
|
10874
10899
|
if (!position) {
|
|
10875
10900
|
return false;
|
|
10876
10901
|
}
|
|
10902
|
+
if (documentHoldCount) {
|
|
10903
|
+
// There is a place to go back to, and the list holding the document is
|
|
10904
|
+
// putting it back (see holdDocumentScroll).
|
|
10905
|
+
return true;
|
|
10906
|
+
}
|
|
10877
10907
|
scrollTo(position);
|
|
10878
10908
|
return true;
|
|
10879
10909
|
};
|
|
@@ -15246,230 +15276,814 @@ const useOpenControllerByProps = (props, name) => {
|
|
|
15246
15276
|
};
|
|
15247
15277
|
|
|
15248
15278
|
/**
|
|
15249
|
-
*
|
|
15250
|
-
*
|
|
15279
|
+
* Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
|
|
15280
|
+
* (non-top-layer) renderers — operate on a plain DOM element, no knowledge
|
|
15281
|
+
* of which of the two owns it.
|
|
15282
|
+
*/
|
|
15283
|
+
|
|
15284
|
+
|
|
15285
|
+
/**
|
|
15286
|
+
* Whether a visibleRectEffect delivery is one that can have taken height away
|
|
15287
|
+
* from a popup, and so pushed whatever holds focus out of sight:
|
|
15288
|
+
* - "resize": the window/visual viewport settled — which is also how the
|
|
15289
|
+
* on-screen keyboard arrives, overlay or not (window_size.js);
|
|
15290
|
+
* - ELEMENT_SIZE_CHANGE: the popup's own box measured different;
|
|
15291
|
+
* - "focusin": the focus-settled re-measure, for the room that changes with
|
|
15292
|
+
* nothing announcing it (subscribeFocusSettled in window_size.js).
|
|
15251
15293
|
*
|
|
15252
|
-
*
|
|
15253
|
-
*
|
|
15254
|
-
*
|
|
15255
|
-
|
|
15256
|
-
|
|
15257
|
-
|
|
15258
|
-
|
|
15259
|
-
|
|
15294
|
+
* Everything else is a scroll of one kind or another, where nothing got
|
|
15295
|
+
* smaller and scrolling the focused element back would fight the very gesture
|
|
15296
|
+
* that fired it.
|
|
15297
|
+
*/
|
|
15298
|
+
const mayHaveHiddenFocus = (event) => {
|
|
15299
|
+
const type = event?.type;
|
|
15300
|
+
return (
|
|
15301
|
+
type === "resize" || type === ELEMENT_SIZE_CHANGE || type === "focusin"
|
|
15302
|
+
);
|
|
15303
|
+
};
|
|
15304
|
+
|
|
15305
|
+
const scrollportHeightMap = new WeakMap();
|
|
15306
|
+
/**
|
|
15307
|
+
* Scrolls whatever holds focus inside `popupEl` back into view, if the popup
|
|
15308
|
+
* getting shorter has pushed it out.
|
|
15260
15309
|
*
|
|
15261
|
-
*
|
|
15262
|
-
*
|
|
15263
|
-
*
|
|
15264
|
-
*
|
|
15265
|
-
*
|
|
15266
|
-
*
|
|
15310
|
+
* The case this exists for: a field low in the scrolling body of a popup that
|
|
15311
|
+
* also has a footer (box.jsx — with a body, the body is the only thing that
|
|
15312
|
+
* scrolls and the footer is a sibling sitting right under it). Focusing the
|
|
15313
|
+
* field makes the browser scroll it into view, which it does against the
|
|
15314
|
+
* popup's height AT THAT MOMENT; the on-screen keyboard then opens and takes
|
|
15315
|
+
* that height away. The body shrinks, its scrollTop does not move, so the
|
|
15316
|
+
* content slides down relative to the shorter scrollport and the field ends up
|
|
15317
|
+
* past its bottom edge — visually, swallowed by the footer. The browser does
|
|
15318
|
+
* not redo a scroll-into-view it already answered, so this does.
|
|
15267
15319
|
*
|
|
15268
|
-
*
|
|
15269
|
-
*
|
|
15270
|
-
*
|
|
15271
|
-
*
|
|
15272
|
-
* the
|
|
15273
|
-
*
|
|
15320
|
+
* Only what the shrink itself hid, though: with the keyboard up the user reads
|
|
15321
|
+
* the rest of the popup by scrolling the field away — to reach the submit under
|
|
15322
|
+
* it, typically — and the room keeps changing while they do (a keyboard settling
|
|
15323
|
+
* in two steps, a suggestion strip, a browser bar). Answering each of those by
|
|
15324
|
+
* scrolling the field back takes the popup away from wherever they had just
|
|
15325
|
+
* scrolled it, over and over: what they were reading cannot be reached at all
|
|
15326
|
+
* without blurring the field first, and the popup reads as unscrollable. So the
|
|
15327
|
+
* field is brought back only when it was in view before the room shrank, which
|
|
15328
|
+
* one remembered number answers: a resize moves neither scrollTop nor the
|
|
15329
|
+
* element's offset inside the scrolled content, so measuring against the height
|
|
15330
|
+
* the scrollport HAD is measuring the state before the change.
|
|
15274
15331
|
*
|
|
15275
|
-
*
|
|
15276
|
-
*
|
|
15332
|
+
* Scoped to the field's own scroll container (never the page): a popup traps
|
|
15333
|
+
* scrolling precisely so the document underneath cannot move, and a plain
|
|
15334
|
+
* scrollIntoView walks past a container whose scrollbar isn't visible — see
|
|
15335
|
+
* scrollIntoViewScoped's own doc.
|
|
15336
|
+
*
|
|
15337
|
+
* "nearest": the smallest scroll that makes it visible, and none at all when it
|
|
15338
|
+
* already is. Where it lands is the container's own business — a navi scroller
|
|
15339
|
+
* keeps a band free at its edges so a field never comes back glued to one (see
|
|
15340
|
+
* scroll-padding in box.jsx).
|
|
15277
15341
|
*/
|
|
15278
|
-
const
|
|
15279
|
-
|
|
15280
|
-
|
|
15281
|
-
|
|
15282
|
-
|
|
15283
|
-
|
|
15284
|
-
|
|
15285
|
-
|
|
15286
|
-
|
|
15287
|
-
|
|
15288
|
-
|
|
15289
|
-
|
|
15290
|
-
|
|
15342
|
+
const keepFocusedElementVisible = (popupEl) => {
|
|
15343
|
+
const { activeElement } = document;
|
|
15344
|
+
if (!activeElement || activeElement === popupEl) {
|
|
15345
|
+
return;
|
|
15346
|
+
}
|
|
15347
|
+
if (!popupEl.contains(activeElement)) {
|
|
15348
|
+
return;
|
|
15349
|
+
}
|
|
15350
|
+
const scrollContainer = getScrollContainer(activeElement);
|
|
15351
|
+
if (!scrollContainer || !popupEl.contains(scrollContainer)) {
|
|
15352
|
+
// What scrolls the field is outside the popup, which means the page: a
|
|
15353
|
+
// popup holds it still on purpose (trapScrollInside), so there is nothing
|
|
15354
|
+
// here to scroll back.
|
|
15355
|
+
return;
|
|
15356
|
+
}
|
|
15357
|
+
const scrollportHeight = scrollContainer.clientHeight;
|
|
15358
|
+
const scrollportHeightBefore = scrollportHeightMap.get(scrollContainer);
|
|
15359
|
+
scrollportHeightMap.set(scrollContainer, scrollportHeight);
|
|
15360
|
+
if (scrollportHeightBefore !== undefined) {
|
|
15361
|
+
if (scrollportHeight >= scrollportHeightBefore) {
|
|
15362
|
+
// Nothing was taken away, so nothing was hidden by this.
|
|
15363
|
+
return;
|
|
15364
|
+
}
|
|
15365
|
+
if (
|
|
15366
|
+
!isVisibleInScrollport(
|
|
15367
|
+
activeElement,
|
|
15368
|
+
scrollContainer,
|
|
15369
|
+
scrollportHeightBefore,
|
|
15370
|
+
)
|
|
15371
|
+
) {
|
|
15372
|
+
return;
|
|
15291
15373
|
}
|
|
15292
15374
|
}
|
|
15293
|
-
|
|
15294
|
-
|
|
15295
|
-
|
|
15296
|
-
const useCalloutRequestClose = () => {
|
|
15297
|
-
return useContext(CalloutContext)?.requestClose;
|
|
15298
|
-
};
|
|
15299
|
-
const useCalloutElement = () => {
|
|
15300
|
-
return useContext(CalloutContext)?.element;
|
|
15301
|
-
};
|
|
15302
|
-
const renderIntoCallout = (jsx$1, calloutMessageElement, {
|
|
15303
|
-
requestClose,
|
|
15304
|
-
element
|
|
15305
|
-
}) => {
|
|
15306
|
-
const calloutJsx = jsx(CalloutContext.Provider, {
|
|
15307
|
-
value: {
|
|
15308
|
-
requestClose,
|
|
15309
|
-
element
|
|
15310
|
-
},
|
|
15311
|
-
children: jsx$1
|
|
15375
|
+
scrollIntoViewScoped(activeElement, {
|
|
15376
|
+
container: scrollContainer,
|
|
15377
|
+
block: "nearest",
|
|
15312
15378
|
});
|
|
15313
|
-
render(calloutJsx, calloutMessageElement);
|
|
15314
15379
|
};
|
|
15315
15380
|
|
|
15316
|
-
//
|
|
15317
|
-
//
|
|
15318
|
-
//
|
|
15319
|
-
//
|
|
15320
|
-
const
|
|
15321
|
-
|
|
15322
|
-
|
|
15323
|
-
|
|
15324
|
-
const
|
|
15325
|
-
|
|
15326
|
-
|
|
15327
|
-
|
|
15328
|
-
element
|
|
15329
|
-
});
|
|
15381
|
+
// Whether any part of `el` was showing in `container` back when its scrollport
|
|
15382
|
+
// was `scrollportHeight` tall. Both boxes are read now: the container may have
|
|
15383
|
+
// moved as well as shrunk (a centered dialog re-centers itself), and the
|
|
15384
|
+
// difference between the two tops is what that move leaves alone.
|
|
15385
|
+
const isVisibleInScrollport = (el, container, scrollportHeight) => {
|
|
15386
|
+
const elRect = el.getBoundingClientRect();
|
|
15387
|
+
const containerRect = container.getBoundingClientRect();
|
|
15388
|
+
const elTop = elRect.top - containerRect.top + container.scrollTop;
|
|
15389
|
+
const elBottom = elTop + elRect.height;
|
|
15390
|
+
const scrollportTop = container.scrollTop;
|
|
15391
|
+
const scrollportBottom = scrollportTop + scrollportHeight;
|
|
15392
|
+
return elBottom > scrollportTop && elTop < scrollportBottom;
|
|
15330
15393
|
};
|
|
15331
15394
|
|
|
15332
|
-
|
|
15333
|
-
|
|
15334
|
-
|
|
15335
|
-
|
|
15336
|
-
|
|
15337
|
-
|
|
15338
|
-
|
|
15339
|
-
|
|
15340
|
-
|
|
15341
|
-
|
|
15342
|
-
|
|
15343
|
-
|
|
15344
|
-
|
|
15395
|
+
/**
|
|
15396
|
+
* Calls `onSettled` once `el`'s current CSS transition is over — via
|
|
15397
|
+
* `transitionend`, with a safety `setTimeout` fallback matching the longest
|
|
15398
|
+
* `transition-duration`, in case nothing actually transitions or an event is
|
|
15399
|
+
* missed.
|
|
15400
|
+
*
|
|
15401
|
+
* Returns a "cancel" function, so a caller whose instance has been superseded
|
|
15402
|
+
* (a fresh open/close about to set its own state) can keep this stale one from
|
|
15403
|
+
* firing later. Cancelling only stops `onSettled`: undoing whatever the caller
|
|
15404
|
+
* did up front is that fresh call's business, not this one's.
|
|
15405
|
+
*/
|
|
15406
|
+
const whenTransitionSettles = (el, onSettled) => {
|
|
15407
|
+
let settled = false;
|
|
15408
|
+
const onTransitionEnd = (transitionEvent) => {
|
|
15409
|
+
if (transitionEvent.target === el) {
|
|
15410
|
+
finish();
|
|
15345
15411
|
}
|
|
15346
|
-
|
|
15347
|
-
|
|
15412
|
+
};
|
|
15413
|
+
const stopWatching = () => {
|
|
15414
|
+
settled = true;
|
|
15415
|
+
el.removeEventListener("transitionend", onTransitionEnd);
|
|
15416
|
+
clearTimeout(safetyTimeoutId);
|
|
15417
|
+
};
|
|
15418
|
+
const finish = () => {
|
|
15419
|
+
if (settled) {
|
|
15420
|
+
return;
|
|
15348
15421
|
}
|
|
15349
|
-
|
|
15350
|
-
|
|
15351
|
-
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15422
|
+
stopWatching();
|
|
15423
|
+
onSettled();
|
|
15424
|
+
};
|
|
15425
|
+
el.addEventListener("transitionend", onTransitionEnd);
|
|
15426
|
+
const durationsInSeconds = getComputedStyle(el)
|
|
15427
|
+
.transitionDuration.split(",")
|
|
15428
|
+
.map((value) => parseFloat(value) || 0);
|
|
15429
|
+
const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
|
|
15430
|
+
const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
|
|
15431
|
+
return () => {
|
|
15432
|
+
if (settled) {
|
|
15433
|
+
return;
|
|
15355
15434
|
}
|
|
15356
|
-
|
|
15357
|
-
}
|
|
15358
|
-
return vnodes;
|
|
15435
|
+
stopWatching();
|
|
15436
|
+
};
|
|
15359
15437
|
};
|
|
15360
15438
|
|
|
15361
|
-
installImportMetaCssBuild(import.meta);
|
|
15362
15439
|
/**
|
|
15363
|
-
*
|
|
15364
|
-
*
|
|
15365
|
-
*
|
|
15366
|
-
*
|
|
15367
|
-
*
|
|
15440
|
+
* Drops the document's text selection when it lives inside `el`, leaving a
|
|
15441
|
+
* selection made elsewhere on the page alone.
|
|
15442
|
+
*
|
|
15443
|
+
* A popup being closed takes its content with it, and a selection is a claim
|
|
15444
|
+
* on content the user can still act on: kept, it would outlive the surface it
|
|
15445
|
+
* was made on — painted on the box for the length of its exit transition, with
|
|
15446
|
+
* the handles and the copy toolbar a phone draws from the live selection
|
|
15447
|
+
* hanging over something that is going away — and reappear with the box on
|
|
15448
|
+
* the next opening. Dropping the selection is the only way to remove that
|
|
15449
|
+
* chrome: a `user-select: none` on the closing box hides the highlight in some
|
|
15450
|
+
* browsers only, and the handles are the browser's own, drawn from the
|
|
15451
|
+
* selection object rather than from any style.
|
|
15368
15452
|
*/
|
|
15369
|
-
|
|
15370
|
-
const
|
|
15371
|
-
|
|
15372
|
-
|
|
15373
|
-
aspect-ratio: 1;
|
|
15374
|
-
color: #fff;
|
|
15375
|
-
vertical-align: middle;
|
|
15376
|
-
background-color: var(--x-callout-status-icon-color);
|
|
15377
|
-
border-radius: 2px;
|
|
15378
|
-
flex-shrink: 0;
|
|
15379
|
-
justify-content: center;
|
|
15380
|
-
align-items: center;
|
|
15381
|
-
width: 1em;
|
|
15382
|
-
height: 1em;
|
|
15383
|
-
display: inline-flex;
|
|
15384
|
-
|
|
15385
|
-
&[data-status="success"] {
|
|
15386
|
-
--x-callout-status-icon-color: var(--navi-callout-success-color);
|
|
15453
|
+
const clearTextSelectionInside = (el) => {
|
|
15454
|
+
const selection = window.getSelection();
|
|
15455
|
+
if (!selection || selection.rangeCount === 0) {
|
|
15456
|
+
return;
|
|
15387
15457
|
}
|
|
15388
|
-
|
|
15389
|
-
|
|
15390
|
-
|
|
15458
|
+
const range = selection.getRangeAt(0);
|
|
15459
|
+
if (!el.contains(range.commonAncestorContainer)) {
|
|
15460
|
+
return;
|
|
15391
15461
|
}
|
|
15462
|
+
selection.removeAllRanges();
|
|
15463
|
+
};
|
|
15392
15464
|
|
|
15393
|
-
|
|
15394
|
-
|
|
15395
|
-
|
|
15465
|
+
/**
|
|
15466
|
+
* Disables pointer-events on `el` until its current CSS transition settles —
|
|
15467
|
+
* avoids the cursor changing/something becoming clickable while the popup is
|
|
15468
|
+
* still visually moving into or out of place.
|
|
15469
|
+
*
|
|
15470
|
+
* Returns whenTransitionSettles' own "cancel" function: it doesn't restore
|
|
15471
|
+
* pointer-events, since a fresh call for the next open/close is about to set
|
|
15472
|
+
* its own state.
|
|
15473
|
+
*/
|
|
15474
|
+
const suppressPointerEventsDuringTransition = (el) => {
|
|
15475
|
+
el.style.pointerEvents = "none";
|
|
15476
|
+
return whenTransitionSettles(el, () => {
|
|
15477
|
+
el.style.pointerEvents = "";
|
|
15478
|
+
});
|
|
15479
|
+
};
|
|
15396
15480
|
|
|
15397
|
-
|
|
15398
|
-
|
|
15481
|
+
/**
|
|
15482
|
+
* Hides the backdrop, deferring until the browser's matching "click" fires
|
|
15483
|
+
* when `closeEvent` was triggered by a mousedown (see popover.jsx's top
|
|
15484
|
+
* comment for why) — same capture-phase-on-document pattern as
|
|
15485
|
+
* armSuppressNextOpenRequest in open_controller.js, which a plain timeout
|
|
15486
|
+
* can't safely replace: mouseup (and the click that follows it) can land an
|
|
15487
|
+
* arbitrarily long time after mousedown (the user is still holding the
|
|
15488
|
+
* button down), so a short timeout can fire first and hide the backdrop
|
|
15489
|
+
* before its own click ever arrives. A capture-phase listener on document
|
|
15490
|
+
* fires for every click regardless of what any bubble-phase handler does
|
|
15491
|
+
* downstream, so no fallback timer is needed.
|
|
15492
|
+
*
|
|
15493
|
+
* `hide` is the caller's own way to actually hide the backdrop
|
|
15494
|
+
* (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
|
|
15495
|
+
* for a plain div) — this helper only owns the mousedown/click timing.
|
|
15496
|
+
*
|
|
15497
|
+
* Returns a disarm function (or undefined if hidden immediately), so a
|
|
15498
|
+
* fresh open can cancel a pending hide it's about to make redundant.
|
|
15499
|
+
*/
|
|
15500
|
+
const armPointerDownOutsideClose = (closeEvent, hide) => {
|
|
15501
|
+
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
15502
|
+
if (!mousedownEvent) {
|
|
15503
|
+
hide();
|
|
15504
|
+
return undefined;
|
|
15399
15505
|
}
|
|
15506
|
+
// `hide` is handed the click, for a wall with peers to re-aim it (see
|
|
15507
|
+
// handWallClickToPeer).
|
|
15508
|
+
const onClick = (clickEvent) => {
|
|
15509
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
15510
|
+
hide(clickEvent);
|
|
15511
|
+
};
|
|
15512
|
+
document.addEventListener("click", onClick, { capture: true });
|
|
15513
|
+
return () => {
|
|
15514
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
15515
|
+
};
|
|
15516
|
+
};
|
|
15400
15517
|
|
|
15401
|
-
|
|
15402
|
-
|
|
15518
|
+
/**
|
|
15519
|
+
* Maps a positionArea y/x pair to a concrete `navi-animation` value (a
|
|
15520
|
+
* `prefix` plus a direction word), or `null` if both axes overlap the anchor
|
|
15521
|
+
* (no direction at all — that's `resolvedAnimationKind === "scaling"`
|
|
15522
|
+
* territory instead, see resolveAutoAnimationKind below).
|
|
15523
|
+
*
|
|
15524
|
+
* `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
|
|
15525
|
+
* when docked) keeps the word as the compass direction the popup comes
|
|
15526
|
+
* from: placed "top" (a point/corner), it slides in from the top.
|
|
15527
|
+
* `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
|
|
15528
|
+
* direction instead, the opposite compass point: placed "top" of the
|
|
15529
|
+
* anchor, it moves/grows up, away from the anchor (which sits below it).
|
|
15530
|
+
*
|
|
15531
|
+
* "inset-*"/"center" contribute no direction on their axis either way.
|
|
15532
|
+
*/
|
|
15533
|
+
const resolveDirectionValue = (y, x, { prefix }) => {
|
|
15534
|
+
const yWord =
|
|
15535
|
+
y === "top"
|
|
15536
|
+
? prefix === "expand"
|
|
15537
|
+
? "up"
|
|
15538
|
+
: "top"
|
|
15539
|
+
: y === "bottom"
|
|
15540
|
+
? prefix === "expand"
|
|
15541
|
+
? "down"
|
|
15542
|
+
: "bottom"
|
|
15543
|
+
: null;
|
|
15544
|
+
const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
|
|
15545
|
+
if (!yWord && !xWord) {
|
|
15546
|
+
return null;
|
|
15403
15547
|
}
|
|
15548
|
+
return yWord && xWord
|
|
15549
|
+
? `${prefix}-${yWord}-${xWord}`
|
|
15550
|
+
: `${prefix}-${yWord || xWord}`;
|
|
15551
|
+
};
|
|
15404
15552
|
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
|
|
15409
|
-
|
|
15553
|
+
/**
|
|
15554
|
+
* Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
|
|
15555
|
+
* — picked for any real anchor, or for a point/corner placed dead-center
|
|
15556
|
+
* (both positionArea axes overlapping — there's no sensible direction to
|
|
15557
|
+
* slide from in that case). "sliding" otherwise. `anchor` is `undefined`
|
|
15558
|
+
* for any no-anchor/docked case (Dialog always, Popover's own custom
|
|
15559
|
+
* renderer when there's no real anchor), so this collapses to "scaling"
|
|
15560
|
+
* there only for the dead-center case, "sliding" otherwise. The two
|
|
15561
|
+
* "overlapping" booleans below describe the *positionArea* itself (a bare
|
|
15562
|
+
* word vs. "inset-"/"center"), not anything about the anchor — they'd
|
|
15563
|
+
* mean exactly the same thing even with no anchor at all, since it's the
|
|
15564
|
+
* position strategy, not the anchor, that decides whether there's a
|
|
15565
|
+
* direction to slide from.
|
|
15566
|
+
*/
|
|
15567
|
+
const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
|
|
15568
|
+
const yIsOverlapping =
|
|
15569
|
+
parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
|
|
15570
|
+
const xIsOverlapping =
|
|
15571
|
+
parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
|
|
15572
|
+
return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
|
|
15573
|
+
};
|
|
15410
15574
|
|
|
15411
|
-
|
|
15412
|
-
|
|
15413
|
-
|
|
15414
|
-
|
|
15415
|
-
|
|
15575
|
+
/**
|
|
15576
|
+
* A popup reads "outside" from its own border box: what a press lands on
|
|
15577
|
+
* decides nothing on its own (a genuine backdrop press and a press on the
|
|
15578
|
+
* popup's padding both report the popup element as their target, there being
|
|
15579
|
+
* no real ::backdrop node to be one), so the rectangle is what tells them
|
|
15580
|
+
* apart. That holds as long as the box and what the popup paints are the same
|
|
15581
|
+
* thing — which stops being true for a popup with no surface of its own
|
|
15582
|
+
* (`backgroundColor="transparent"`, no shadow, no padding): what the eye reads
|
|
15583
|
+
* as backdrop is then inside the box, and a press there stays a press on the
|
|
15584
|
+
* popup.
|
|
15585
|
+
*
|
|
15586
|
+
* `data-navi-popup-outside` is how a caller says which of its own boxes are
|
|
15587
|
+
* not the surface. It is opt-in because navi cannot infer it — a background
|
|
15588
|
+
* can come from anywhere — while the caller who made the popup see-through
|
|
15589
|
+
* knows exactly which box is decoration and which is paper.
|
|
15590
|
+
*
|
|
15591
|
+
* The marker answers for the element it is on, never for its descendants: a
|
|
15592
|
+
* box painted inside a marked one is still surface, so a row can be marked and
|
|
15593
|
+
* its empty halves read as backdrop while presses on the controls it holds do
|
|
15594
|
+
* not dismiss anything. A descendant with `pointer-events: none` never becomes
|
|
15595
|
+
* a press target at all, so the marked box answers in its place — while the
|
|
15596
|
+
* same declaration on the marked box itself takes it out of hit-testing and
|
|
15597
|
+
* makes the marker unreachable (see warnAboutUnreachableOutsideRegions).
|
|
15598
|
+
*/
|
|
15599
|
+
const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
|
|
15416
15600
|
|
|
15417
15601
|
/**
|
|
15418
|
-
*
|
|
15419
|
-
*
|
|
15420
|
-
*
|
|
15421
|
-
*
|
|
15422
|
-
*
|
|
15602
|
+
* `data-navi-popup-inside` is the other direction: a box of the PAGE the
|
|
15603
|
+
* caller says belongs to a popup, so a press on it is not outside that popup
|
|
15604
|
+
* even though it lies beyond the border box — a card whose press is what
|
|
15605
|
+
* fills the one panel of a board. Its value names the popup by `id`, the way
|
|
15606
|
+
* `commandfor` does (several ids, space separated), because the exemption is
|
|
15607
|
+
* one popup's: a menu open beside the board still closes on the same press.
|
|
15608
|
+
*
|
|
15609
|
+
* Unlike the outside marker it answers for its whole subtree: the outside
|
|
15610
|
+
* marker carves free space out of a surface whose controls stay surface, this
|
|
15611
|
+
* one adds a whole thing — the card, its count, its buttons — to the popup's
|
|
15612
|
+
* own ground, and nothing in it is a dismissal.
|
|
15423
15613
|
*/
|
|
15424
|
-
const
|
|
15425
|
-
const CALLOUT_STATUS_GLYPH_PATH = "m232.5,121 8,196h59l8-196zm36.5,224a37,37 0 1,0 2,0z";
|
|
15614
|
+
const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
|
|
15426
15615
|
|
|
15427
15616
|
/**
|
|
15428
|
-
*
|
|
15429
|
-
*
|
|
15430
|
-
*
|
|
15431
|
-
*
|
|
15432
|
-
*
|
|
15433
|
-
*
|
|
15434
|
-
*
|
|
15435
|
-
* icon, or a circle.
|
|
15617
|
+
* `data-navi-popup-group` names the peers of a popup: the triggers of the other
|
|
15618
|
+
* popups a press goes to from this one — a menu bar, where pressing another
|
|
15619
|
+
* menu while one is open switches to it, in that one press. A trigger joins by
|
|
15620
|
+
* carrying the name (a Picker's `popupGroup` writes it on its own), and a popup
|
|
15621
|
+
* reads it off what the press landed on.
|
|
15622
|
+
*
|
|
15623
|
+
* Returns the peer `target` is in, or null.
|
|
15436
15624
|
*/
|
|
15437
|
-
const
|
|
15438
|
-
|
|
15439
|
-
|
|
15440
|
-
}
|
|
15441
|
-
|
|
15442
|
-
return jsx("span", {
|
|
15443
|
-
className: "navi_callout_status_icon",
|
|
15444
|
-
"data-status": status === "none" ? undefined : status,
|
|
15445
|
-
"data-shape": shape === "circle" ? "circle" : undefined,
|
|
15446
|
-
"aria-hidden": "true",
|
|
15447
|
-
children: jsx("svg", {
|
|
15448
|
-
viewBox: CALLOUT_STATUS_GLYPH_VIEWBOX,
|
|
15449
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
15450
|
-
children: jsx("path", {
|
|
15451
|
-
fill: "currentColor",
|
|
15452
|
-
d: CALLOUT_STATUS_GLYPH_PATH
|
|
15453
|
-
})
|
|
15454
|
-
})
|
|
15455
|
-
});
|
|
15625
|
+
const findPopupPeer = (target, group) => {
|
|
15626
|
+
if (!group || !target || typeof target.closest !== "function") {
|
|
15627
|
+
return null;
|
|
15628
|
+
}
|
|
15629
|
+
return target.closest(`[data-navi-popup-group="${CSS.escape(group)}"]`);
|
|
15456
15630
|
};
|
|
15457
15631
|
|
|
15458
|
-
|
|
15459
|
-
|
|
15460
|
-
|
|
15461
|
-
let
|
|
15462
|
-
|
|
15463
|
-
|
|
15464
|
-
|
|
15465
|
-
|
|
15466
|
-
|
|
15632
|
+
/**
|
|
15633
|
+
* The click a wall took, handed to the peer (see findPopupPeer) it covered.
|
|
15634
|
+
*
|
|
15635
|
+
* A wall cannot let one press through and not another: it is what the pointer
|
|
15636
|
+
* hits, and that is the whole of how it spends a press. So it goes on taking
|
|
15637
|
+
* every one — the page behind stays as unhoverable and unpressable as it looks
|
|
15638
|
+
* — and once the press is over, its click is re-aimed at what the wall stood
|
|
15639
|
+
* over at that point, when that is a peer. The click alone, for the reason
|
|
15640
|
+
* transition_press.js gives: a pointer stream made up with no pointer behind it
|
|
15641
|
+
* starts gestures (a hold, a drag) that nothing ever ends, and a click is what
|
|
15642
|
+
* opens a popup's trigger.
|
|
15643
|
+
*
|
|
15644
|
+
* Called with the click that takes the wall down after a press closed the
|
|
15645
|
+
* popup (see armPointerDownOutsideClose), so the peer opens as the popup it
|
|
15646
|
+
* replaces goes.
|
|
15647
|
+
*/
|
|
15648
|
+
const handWallClickToPeer = (
|
|
15649
|
+
clickEvent,
|
|
15650
|
+
{ wallEl, popupEl, anchorEl, group },
|
|
15651
|
+
) => {
|
|
15652
|
+
if (!clickEvent.isTrusted || clickEvent.target !== wallEl) {
|
|
15653
|
+
// A click dispatched by hand is already aimed, and one that did not land
|
|
15654
|
+
// on the wall (a press on the popup's own outside region) was no wall's.
|
|
15655
|
+
return;
|
|
15656
|
+
}
|
|
15657
|
+
// The topmost element under the wall at that point: what the press would
|
|
15658
|
+
// have landed on with no wall. Asked past the wall rather than after hiding
|
|
15659
|
+
// it, which a fading wall may not be yet.
|
|
15660
|
+
const covered = document
|
|
15661
|
+
.elementsFromPoint(clickEvent.clientX, clickEvent.clientY)
|
|
15662
|
+
.find((el) => el !== wallEl && !popupEl.contains(el));
|
|
15663
|
+
const peer = findPopupPeer(covered, group);
|
|
15664
|
+
if (!peer) {
|
|
15665
|
+
return;
|
|
15666
|
+
}
|
|
15667
|
+
if (peer.contains(popupEl) || (anchorEl && peer.contains(anchorEl))) {
|
|
15668
|
+
// The popup's own trigger — the one holding it (a picker renders its
|
|
15669
|
+
// popup inside itself) or the one it opened against: the press through
|
|
15670
|
+
// the wall closed the popup, and handing the trigger its click would open
|
|
15671
|
+
// it again.
|
|
15672
|
+
return;
|
|
15673
|
+
}
|
|
15674
|
+
// The page sees one click, on the peer, rather than two.
|
|
15675
|
+
clickEvent.stopPropagation();
|
|
15676
|
+
// A PointerEvent where the browser makes clicks of them, a MouseEvent
|
|
15677
|
+
// elsewhere: the same kind, copied field by field.
|
|
15678
|
+
const ClickEvent = clickEvent.constructor;
|
|
15679
|
+
covered.dispatchEvent(new ClickEvent(clickEvent.type, clickEvent));
|
|
15680
|
+
};
|
|
15467
15681
|
|
|
15468
|
-
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
|
|
15472
|
-
|
|
15682
|
+
/**
|
|
15683
|
+
* What a press landing on a region the caller declared as not-its-surface
|
|
15684
|
+
* does: exactly what the same press on the backdrop would do.
|
|
15685
|
+
*
|
|
15686
|
+
* Lives on the popup's own content element rather than in the backdrop's
|
|
15687
|
+
* handler, for both renderers: the backdrop is a sibling behind the popup
|
|
15688
|
+
* (never an ancestor), so a press inside the popup's box never reaches it.
|
|
15689
|
+
*/
|
|
15690
|
+
const handlePressOnOutsideRegion = (
|
|
15691
|
+
mouseDownEvent,
|
|
15692
|
+
{ popupEl, openController, pressOutside },
|
|
15693
|
+
) => {
|
|
15694
|
+
if (mouseDownEvent.button !== 0) {
|
|
15695
|
+
return;
|
|
15696
|
+
}
|
|
15697
|
+
const { target } = mouseDownEvent;
|
|
15698
|
+
if (!target.hasAttribute(OUTSIDE_REGION_ATTRIBUTE)) {
|
|
15699
|
+
return;
|
|
15700
|
+
}
|
|
15701
|
+
if (openedDuringThisPress(openController)) {
|
|
15702
|
+
// The release of the press that opened it is not somebody dismissing it
|
|
15703
|
+
// (see openedDuringThisPress).
|
|
15704
|
+
return;
|
|
15705
|
+
}
|
|
15706
|
+
// A popup opens inside its opener's own subtree, so a press on a region
|
|
15707
|
+
// belonging to a popup nested in this one bubbles through here too — and it
|
|
15708
|
+
// is a press on what is in front, not on this popup's own decoration.
|
|
15709
|
+
if (
|
|
15710
|
+
target.closest(`[navi-control="dialog"], [navi-control="popover"]`) !==
|
|
15711
|
+
popupEl
|
|
15712
|
+
) {
|
|
15713
|
+
return;
|
|
15714
|
+
}
|
|
15715
|
+
if (pressOutside === "capture") {
|
|
15716
|
+
mouseDownEvent.preventDefault();
|
|
15717
|
+
return;
|
|
15718
|
+
}
|
|
15719
|
+
if (pressOutside === "close" || pressOutside === "cancel") {
|
|
15720
|
+
openController.requestClose(mouseDownEvent, {
|
|
15721
|
+
isCancel: pressOutside === "cancel",
|
|
15722
|
+
});
|
|
15723
|
+
}
|
|
15724
|
+
};
|
|
15725
|
+
|
|
15726
|
+
/**
|
|
15727
|
+
* Hears an outside press from the document itself, for a popup with no
|
|
15728
|
+
* backdrop of its own to catch one: a modal `<dialog>` (whose native
|
|
15729
|
+
* `::backdrop` dispatches nothing a handler can be put on) and any popup the
|
|
15730
|
+
* caller asked to leave the page reachable (`backdrop={false}`).
|
|
15731
|
+
*
|
|
15732
|
+
* The press is read and never taken: no `preventDefault`, no
|
|
15733
|
+
* `stopPropagation`, and the listener is on `document` rather than on
|
|
15734
|
+
* anything covering the page — so what the press landed on hears it too, in
|
|
15735
|
+
* the same gesture, and the popup only takes note. That is the whole point of
|
|
15736
|
+
* a popup with no wall: a press outside is a press on the page, and the page
|
|
15737
|
+
* answers it. The one popup where that cannot hold is the modal dialog, where
|
|
15738
|
+
* the browser has already made everything behind inert before this runs.
|
|
15739
|
+
*
|
|
15740
|
+
* `pressEventType` says which event IS the press, and the two callers want
|
|
15741
|
+
* opposite ones:
|
|
15742
|
+
*
|
|
15743
|
+
* - `"pointerdown"` for a popup with no wall: a page with no wall over it is
|
|
15744
|
+
* a page whose own elements arbitrate their presses, and cancelling a
|
|
15745
|
+
* `pointerdown` — what a drag source and a control keeping the focus where
|
|
15746
|
+
* it is both do — suppresses every compatibility mouse event that would have
|
|
15747
|
+
* followed it. The press happens, reaches its target, and no `mousedown` is
|
|
15748
|
+
* ever dispatched for it. Reading the pointer event is reading the press
|
|
15749
|
+
* itself, whatever anything does with it afterwards, and it is the same
|
|
15750
|
+
* event the drag and surface detectors read.
|
|
15751
|
+
* - `"mousedown"` for the modal dialog, whose native `::backdrop` exists to
|
|
15752
|
+
* spend the press. The page's controls act on `mousedown`, and on a touch
|
|
15753
|
+
* screen that is not the same moment as `pointerdown`: the compatibility
|
|
15754
|
+
* mouse events are synthesized at `touchend` and hit-tested again then.
|
|
15755
|
+
* Closing on `pointerdown` takes the `::backdrop` away before that second
|
|
15756
|
+
* hit-test, so the `mousedown` lands on whatever the page holds under the
|
|
15757
|
+
* finger, and one tap both dismisses the dialog and presses the page. Read
|
|
15758
|
+
* on `mousedown`, the wall is still up when the press the page acts on is
|
|
15759
|
+
* aimed, and it catches it. Nothing behind a modal wall can cancel that
|
|
15760
|
+
* press, since nothing behind it hears the `pointerdown` at all.
|
|
15761
|
+
*
|
|
15762
|
+
* Capture phase, so a handler downstream that stops propagation cannot keep
|
|
15763
|
+
* the popup open either — the press is still outside whatever is made of it.
|
|
15764
|
+
*
|
|
15765
|
+
* Returns a cleanup that removes the listener.
|
|
15766
|
+
*/
|
|
15767
|
+
const armOutsidePressClose = (
|
|
15768
|
+
popupEl,
|
|
15769
|
+
{ openController, pressOutside, pressEventType = "pointerdown" },
|
|
15770
|
+
) => {
|
|
15771
|
+
const onDocumentPointerDown = (pointerDownEvent) => {
|
|
15772
|
+
if (pointerDownEvent.button !== 0) {
|
|
15773
|
+
return;
|
|
15774
|
+
}
|
|
15775
|
+
if (openedDuringThisPress(openController)) {
|
|
15776
|
+
// The release of the press that opened it is not somebody dismissing it
|
|
15777
|
+
// (see openedDuringThisPress).
|
|
15778
|
+
return;
|
|
15779
|
+
}
|
|
15780
|
+
// The press landed inside another popup: that is a press on what is in
|
|
15781
|
+
// front, not outside. Asking the target where it lives rather than asking
|
|
15782
|
+
// this popup whether it was pushed — a popup in front does not have to be
|
|
15783
|
+
// one this popup knows about. In front means neither inside nor around: a
|
|
15784
|
+
// popup nested in this one falls to the containment check below as the
|
|
15785
|
+
// inside press it is, and one this popup opened INSIDE (a bubble rendered
|
|
15786
|
+
// in a sheet's content) is around it, so what that popup holds is outside
|
|
15787
|
+
// this one exactly like the rest of the page.
|
|
15788
|
+
const popupUnderPointer = pointerDownEvent.target.closest?.(
|
|
15789
|
+
`[navi-control="dialog"], [navi-control="popover"]`,
|
|
15790
|
+
);
|
|
15791
|
+
if (
|
|
15792
|
+
popupUnderPointer &&
|
|
15793
|
+
popupUnderPointer !== popupEl &&
|
|
15794
|
+
!popupEl.contains(popupUnderPointer) &&
|
|
15795
|
+
!popupUnderPointer.contains(popupEl)
|
|
15796
|
+
) {
|
|
15797
|
+
return;
|
|
15798
|
+
}
|
|
15799
|
+
if (
|
|
15800
|
+
popupEl.id &&
|
|
15801
|
+
pointerDownEvent.target.closest?.(
|
|
15802
|
+
`[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupEl.id)}"]`,
|
|
15803
|
+
)
|
|
15804
|
+
) {
|
|
15805
|
+
return;
|
|
15806
|
+
}
|
|
15807
|
+
// Real DOM containment wins over the rectangle below — an element
|
|
15808
|
+
// genuinely inside the popup (`overflow: visible`, a negative margin, an
|
|
15809
|
+
// absolutely-positioned child) can be painted outside its border box, and
|
|
15810
|
+
// a press there is not outside just because its coordinates are. The
|
|
15811
|
+
// popup element itself is excluded (contains() answers true for it): a
|
|
15812
|
+
// press on a modal dialog's native backdrop reports the dialog as its
|
|
15813
|
+
// target, there being no `::backdrop` node to be one, so treating that as
|
|
15814
|
+
// contained would keep the rectangle from ever running.
|
|
15815
|
+
if (
|
|
15816
|
+
pointerDownEvent.target !== popupEl &&
|
|
15817
|
+
popupEl.contains(pointerDownEvent.target)
|
|
15818
|
+
) {
|
|
15819
|
+
return;
|
|
15820
|
+
}
|
|
15821
|
+
const rect = popupEl.getBoundingClientRect();
|
|
15822
|
+
const isOutside =
|
|
15823
|
+
pointerDownEvent.clientX < rect.left ||
|
|
15824
|
+
pointerDownEvent.clientX > rect.right ||
|
|
15825
|
+
pointerDownEvent.clientY < rect.top ||
|
|
15826
|
+
pointerDownEvent.clientY > rect.bottom;
|
|
15827
|
+
if (!isOutside) {
|
|
15828
|
+
return;
|
|
15829
|
+
}
|
|
15830
|
+
openController.requestClose(pointerDownEvent, {
|
|
15831
|
+
isCancel: pressOutside === "cancel",
|
|
15832
|
+
});
|
|
15833
|
+
};
|
|
15834
|
+
document.addEventListener(pressEventType, onDocumentPointerDown, {
|
|
15835
|
+
capture: true,
|
|
15836
|
+
});
|
|
15837
|
+
return () => {
|
|
15838
|
+
document.removeEventListener(pressEventType, onDocumentPointerDown, {
|
|
15839
|
+
capture: true,
|
|
15840
|
+
});
|
|
15841
|
+
};
|
|
15842
|
+
};
|
|
15843
|
+
|
|
15844
|
+
/**
|
|
15845
|
+
* Typography reset shared by the surfaces a control opens: callout, popover,
|
|
15846
|
+
* dialog.
|
|
15847
|
+
*
|
|
15848
|
+
* A surface is painted on top of the page but lives in the DOM subtree of the
|
|
15849
|
+
* element it opens from, so every inherited text property of that element
|
|
15850
|
+
* reaches it. The ink an element chose for its own background — centered,
|
|
15851
|
+
* shadowed, uppercase, letter-spaced, kept on one line — arrives on paper that
|
|
15852
|
+
* has none of that background, and the caller discovers it as a symptom
|
|
15853
|
+
* (a blurred tooltip, a centered message) with nothing on screen pointing back
|
|
15854
|
+
* to the rule three elements up. A surface writes its own text, on its own
|
|
15855
|
+
* paper, in the document's terms.
|
|
15856
|
+
*
|
|
15857
|
+
* Every one of them is set to its initial value rather than `revert`: only
|
|
15858
|
+
* `color` and `background-color` are declared by the UA on a `[popover]`/
|
|
15859
|
+
* `<dialog>`, so `revert` on any of these would roll back to the inherited
|
|
15860
|
+
* value — the very thing to stop. `text-align: initial` is `start`, which
|
|
15861
|
+
* still reads the surface's own `direction`, so an RTL document keeps its
|
|
15862
|
+
* side.
|
|
15863
|
+
*
|
|
15864
|
+
* `font-family` is deliberately absent: a surface keeps the face of what
|
|
15865
|
+
* opened it, so a section written in a display font gets its tooltips in that
|
|
15866
|
+
* font. `color`, `font-size` and `font-weight` are absent too — each surface
|
|
15867
|
+
* answers those its own way (a popup writes in --navi-popup-color and follows
|
|
15868
|
+
* the size of what opened it; a callout reverts to the document's ink and
|
|
15869
|
+
* size).
|
|
15870
|
+
*
|
|
15871
|
+
* In `@layer navi` so an app that wants one of these back says so on the
|
|
15872
|
+
* surface and wins, without having to out-specify anything.
|
|
15873
|
+
*/
|
|
15874
|
+
const surfaceTextCss = /* css */ `
|
|
15875
|
+
@layer navi {
|
|
15876
|
+
.navi_callout,
|
|
15877
|
+
.navi_popover,
|
|
15878
|
+
.navi_dialog {
|
|
15879
|
+
font-style: initial;
|
|
15880
|
+
text-align: initial;
|
|
15881
|
+
text-indent: initial;
|
|
15882
|
+
text-transform: initial;
|
|
15883
|
+
text-shadow: none;
|
|
15884
|
+
white-space: initial;
|
|
15885
|
+
word-spacing: initial;
|
|
15886
|
+
letter-spacing: initial;
|
|
15887
|
+
}
|
|
15888
|
+
}
|
|
15889
|
+
`;
|
|
15890
|
+
|
|
15891
|
+
const CalloutContext = createContext();
|
|
15892
|
+
const useCalloutRequestClose = () => {
|
|
15893
|
+
return useContext(CalloutContext)?.requestClose;
|
|
15894
|
+
};
|
|
15895
|
+
const useCalloutElement = () => {
|
|
15896
|
+
return useContext(CalloutContext)?.element;
|
|
15897
|
+
};
|
|
15898
|
+
const renderIntoCallout = (jsx$1, calloutMessageElement, {
|
|
15899
|
+
requestClose,
|
|
15900
|
+
element
|
|
15901
|
+
}) => {
|
|
15902
|
+
const calloutJsx = jsx(CalloutContext.Provider, {
|
|
15903
|
+
value: {
|
|
15904
|
+
requestClose,
|
|
15905
|
+
element
|
|
15906
|
+
},
|
|
15907
|
+
children: jsx$1
|
|
15908
|
+
});
|
|
15909
|
+
render(calloutJsx, calloutMessageElement);
|
|
15910
|
+
};
|
|
15911
|
+
|
|
15912
|
+
// An HTML message is rendered through preact rather than innerHTML: what a
|
|
15913
|
+
// message holds is then a preact tree like any other, so a callout's own JSX
|
|
15914
|
+
// (its context, its close button) works the same whether the message arrived
|
|
15915
|
+
// as markup or as elements.
|
|
15916
|
+
const renderHtmlIntoCallout = (html, calloutMessageElement, {
|
|
15917
|
+
requestClose,
|
|
15918
|
+
element
|
|
15919
|
+
}) => {
|
|
15920
|
+
const template = document.createElement("template");
|
|
15921
|
+
template.innerHTML = html;
|
|
15922
|
+
renderIntoCallout(domToVNodes(template.content), calloutMessageElement, {
|
|
15923
|
+
requestClose,
|
|
15924
|
+
element
|
|
15925
|
+
});
|
|
15926
|
+
};
|
|
15927
|
+
|
|
15928
|
+
// Unmounts whatever preact rendered before the element is filled by hand
|
|
15929
|
+
// (a DOM node, an iframe); innerHTML alone would leave preact believing its
|
|
15930
|
+
// tree is still there.
|
|
15931
|
+
const clearCalloutMessage = calloutMessageElement => {
|
|
15932
|
+
render(null, calloutMessageElement);
|
|
15933
|
+
calloutMessageElement.innerHTML = "";
|
|
15934
|
+
};
|
|
15935
|
+
const domToVNodes = node => {
|
|
15936
|
+
const vnodes = [];
|
|
15937
|
+
for (const child of node.childNodes) {
|
|
15938
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
15939
|
+
vnodes.push(child.data);
|
|
15940
|
+
continue;
|
|
15941
|
+
}
|
|
15942
|
+
if (child.nodeType !== Node.ELEMENT_NODE) {
|
|
15943
|
+
continue;
|
|
15944
|
+
}
|
|
15945
|
+
const props = {};
|
|
15946
|
+
for (const {
|
|
15947
|
+
name,
|
|
15948
|
+
value
|
|
15949
|
+
} of child.attributes) {
|
|
15950
|
+
props[name] = value;
|
|
15951
|
+
}
|
|
15952
|
+
vnodes.push(h(child.localName, props, ...domToVNodes(child)));
|
|
15953
|
+
}
|
|
15954
|
+
return vnodes;
|
|
15955
|
+
};
|
|
15956
|
+
|
|
15957
|
+
installImportMetaCssBuild(import.meta);
|
|
15958
|
+
/**
|
|
15959
|
+
* The icon a callout shows beside its message — a square in the status color
|
|
15960
|
+
* with the "!" glyph — drawable on its own, where the callout is not: on the
|
|
15961
|
+
* trigger that opens one (a picker in callout mode), so what one presses looks
|
|
15962
|
+
* like what it opens. The callout's own template draws from the same glyph
|
|
15963
|
+
* (see calloutTemplate in callout.js).
|
|
15964
|
+
*/
|
|
15965
|
+
|
|
15966
|
+
const css$15 = /* css */`.navi_callout_status_icon {
|
|
15967
|
+
--x-callout-status-icon-color: var(--navi-callout-neutral-color);
|
|
15968
|
+
box-sizing: border-box;
|
|
15969
|
+
aspect-ratio: 1;
|
|
15970
|
+
color: #fff;
|
|
15971
|
+
vertical-align: middle;
|
|
15972
|
+
background-color: var(--x-callout-status-icon-color);
|
|
15973
|
+
border-radius: 2px;
|
|
15974
|
+
flex-shrink: 0;
|
|
15975
|
+
justify-content: center;
|
|
15976
|
+
align-items: center;
|
|
15977
|
+
width: 1em;
|
|
15978
|
+
height: 1em;
|
|
15979
|
+
display: inline-flex;
|
|
15980
|
+
|
|
15981
|
+
&[data-status="success"] {
|
|
15982
|
+
--x-callout-status-icon-color: var(--navi-callout-success-color);
|
|
15983
|
+
}
|
|
15984
|
+
|
|
15985
|
+
&[data-status="info"] {
|
|
15986
|
+
--x-callout-status-icon-color: var(--navi-callout-info-color);
|
|
15987
|
+
}
|
|
15988
|
+
|
|
15989
|
+
&[data-status="warning"] {
|
|
15990
|
+
--x-callout-status-icon-color: var(--navi-callout-warning-color);
|
|
15991
|
+
}
|
|
15992
|
+
|
|
15993
|
+
&[data-status="error"] {
|
|
15994
|
+
--x-callout-status-icon-color: var(--navi-callout-error-color);
|
|
15995
|
+
}
|
|
15996
|
+
|
|
15997
|
+
&[data-shape="circle"] {
|
|
15998
|
+
border-radius: 50%;
|
|
15999
|
+
}
|
|
16000
|
+
|
|
16001
|
+
& svg {
|
|
16002
|
+
width: 100%;
|
|
16003
|
+
height: 100%;
|
|
16004
|
+
}
|
|
16005
|
+
}
|
|
16006
|
+
|
|
16007
|
+
.navi_icon > .navi_callout_status_icon {
|
|
16008
|
+
width: 100%;
|
|
16009
|
+
height: 100%;
|
|
16010
|
+
}
|
|
16011
|
+
`;
|
|
16012
|
+
|
|
16013
|
+
/**
|
|
16014
|
+
* The "!" on a square, viewBox and path both. The box is square and holds the
|
|
16015
|
+
* glyph's margin: the ink is 298 of 540 units tall (55%), centered. Drawn this
|
|
16016
|
+
* way rather than tight around the ink so whoever shows it can give the svg the
|
|
16017
|
+
* whole square — the glyph is then placed by the vector rasteriser, which has
|
|
16018
|
+
* no device pixel grid to miss, at any square size down to a fractional one.
|
|
16019
|
+
*/
|
|
16020
|
+
const CALLOUT_STATUS_GLYPH_VIEWBOX = "0 0 540 540";
|
|
16021
|
+
const CALLOUT_STATUS_GLYPH_PATH = "m232.5,121 8,196h59l8-196zm36.5,224a37,37 0 1,0 2,0z";
|
|
16022
|
+
|
|
16023
|
+
/**
|
|
16024
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
16025
|
+
* status?: "info" | "warning" | "error" | "success" | "none",
|
|
16026
|
+
* shape?: "square" | "circle",
|
|
16027
|
+
* }>}
|
|
16028
|
+
* @param {"info"|"warning"|"error"|"success"|"none"} [status="info"] The color
|
|
16029
|
+
* — the callout's own for that status. `"none"` is the neutral one.
|
|
16030
|
+
* @param {"square"|"circle"} [shape="square"] Square like the callout's own
|
|
16031
|
+
* icon, or a circle.
|
|
16032
|
+
*/
|
|
16033
|
+
const CalloutStatusIcon = ({
|
|
16034
|
+
status = "info",
|
|
16035
|
+
shape = "square"
|
|
16036
|
+
}) => {
|
|
16037
|
+
import.meta.css = [css$15, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
|
|
16038
|
+
return jsx("span", {
|
|
16039
|
+
className: "navi_callout_status_icon",
|
|
16040
|
+
"data-status": status === "none" ? undefined : status,
|
|
16041
|
+
"data-shape": shape === "circle" ? "circle" : undefined,
|
|
16042
|
+
"aria-hidden": "true",
|
|
16043
|
+
children: jsx("svg", {
|
|
16044
|
+
viewBox: CALLOUT_STATUS_GLYPH_VIEWBOX,
|
|
16045
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
16046
|
+
children: jsx("path", {
|
|
16047
|
+
fill: "currentColor",
|
|
16048
|
+
d: CALLOUT_STATUS_GLYPH_PATH
|
|
16049
|
+
})
|
|
16050
|
+
})
|
|
16051
|
+
});
|
|
16052
|
+
};
|
|
16053
|
+
|
|
16054
|
+
installImportMetaCssBuild(import.meta);
|
|
16055
|
+
|
|
16056
|
+
// Unique for the page's lifetime: a caller may write the id in a commandfor.
|
|
16057
|
+
let calloutCount = 0;
|
|
16058
|
+
|
|
16059
|
+
// How far `closeByScroll: true` lets the page move under the callout before
|
|
16060
|
+
// dismissing it: small enough that any deliberate scroll gesture reaches it,
|
|
16061
|
+
// large enough to survive the pixel or two a trackpad rests on.
|
|
16062
|
+
const CLOSE_BY_SCROLL_DISTANCE = 10;
|
|
16063
|
+
|
|
16064
|
+
// The events of a press a `backdrop` callout spends, up to its click (which
|
|
16065
|
+
// closes the callout and is spent there), and `pointercancel`, which says the
|
|
16066
|
+
// browser took the press. No touch events: they are the scroll's, and what
|
|
16067
|
+
// listens to them — a list learning the user took its scroll over — is
|
|
16068
|
+
// watching the finger, not answering the press.
|
|
16069
|
+
const SPENT_PRESS_EVENT_TYPES = [
|
|
16070
|
+
"pointerdown",
|
|
16071
|
+
"pointerup",
|
|
16072
|
+
"pointercancel",
|
|
16073
|
+
"mousedown",
|
|
16074
|
+
"mouseup",
|
|
16075
|
+
"contextmenu",
|
|
16076
|
+
];
|
|
16077
|
+
// Where a press lands on something drawn over the page rather than on the page
|
|
16078
|
+
// itself: a popup of navi's (either layer) or a native top-layer element.
|
|
16079
|
+
const LAYER_SELECTOR =
|
|
16080
|
+
'[popover], dialog, [navi-control="popover"], [navi-control="dialog"]';
|
|
16081
|
+
|
|
16082
|
+
const css$14 = /* css */ `
|
|
16083
|
+
/* jsenv-css-opaque: same as dialog.jsx, cascade order. */
|
|
16084
|
+
@layer navi {
|
|
16085
|
+
.navi_callout {
|
|
16086
|
+
/* A callout is parented to what it explains, so it inherits from it — and
|
|
15473
16087
|
an element that suppressed text selection (a list row, a drag source)
|
|
15474
16088
|
would make its own explanation unselectable. The message is text one
|
|
15475
16089
|
copies. Layered: an app that has a reason to lock its own callouts down
|
|
@@ -15753,6 +16367,17 @@ const css$14 = /* css */ `
|
|
|
15753
16367
|
* the callout opened, so scrolling back cancels nothing but a round trip shorter than the
|
|
15754
16368
|
* distance leaves it open. Unlike an outside click it also dismisses an "error" callout: the
|
|
15755
16369
|
* caller asked for it explicitly.
|
|
16370
|
+
* @param {boolean} [options.backdrop=false] - Spend an outside press on closing the callout,
|
|
16371
|
+
* the way a popup's wall spends it: no link followed, no control pressed, no focus moved, no
|
|
16372
|
+
* hold or drag started under it. Without it that press closes the callout and is answered by
|
|
16373
|
+
* what it landed on too. Not a wall, though: the scroll a finger starts beside the callout
|
|
16374
|
+
* still scrolls, a press on a layer drawn over the page (a sheet the callout's content
|
|
16375
|
+
* opened) is that layer's, and a press that does not close the callout (an "error" one) is
|
|
16376
|
+
* not spent either
|
|
16377
|
+
* @param {string} [options.group] - With `backdrop`: the name the callout's peers carry in
|
|
16378
|
+
* `data-navi-popup-group` — the triggers of other popups, callouts or not. A press inside a
|
|
16379
|
+
* peer still closes the callout but is not spent, so the peer it landed on opens its own
|
|
16380
|
+
* popup in the same press — a menu bar, where pressing another menu switches to it
|
|
15756
16381
|
* @param {Event} [options.openingEvent] - The gesture the callout belongs to. It names the
|
|
15757
16382
|
* opener — `currentTarget` while the event is still dispatching, `target` once it is over,
|
|
15758
16383
|
* so awaiting before opening changes nothing — and the opener is the one part of the anchor
|
|
@@ -15817,6 +16442,8 @@ const openCallout = (
|
|
|
15817
16442
|
closeByPressOutside = status === "info",
|
|
15818
16443
|
closeOnFocusLeave = closeByPressOutside,
|
|
15819
16444
|
closeByScroll = false,
|
|
16445
|
+
backdrop = false,
|
|
16446
|
+
group,
|
|
15820
16447
|
closeButton = true,
|
|
15821
16448
|
openingEvent,
|
|
15822
16449
|
reopen = "toggle",
|
|
@@ -15901,6 +16528,9 @@ const openCallout = (
|
|
|
15901
16528
|
reason,
|
|
15902
16529
|
});
|
|
15903
16530
|
};
|
|
16531
|
+
// Asked again at each press rather than once: `update` changes both.
|
|
16532
|
+
const pressOutsideCloses = () =>
|
|
16533
|
+
closeByPressOutside && callout.status !== "error";
|
|
15904
16534
|
const onRequestClose = (event) => {
|
|
15905
16535
|
if (!callout.opened) {
|
|
15906
16536
|
return;
|
|
@@ -15909,10 +16539,7 @@ const openCallout = (
|
|
|
15909
16539
|
const clickOrSpaceOutside =
|
|
15910
16540
|
reason === "click_outside" || reason === "space_outside";
|
|
15911
16541
|
if (clickOrSpaceOutside) {
|
|
15912
|
-
if (!
|
|
15913
|
-
return;
|
|
15914
|
-
}
|
|
15915
|
-
if (callout.status === "error") {
|
|
16542
|
+
if (!pressOutsideCloses()) {
|
|
15916
16543
|
return;
|
|
15917
16544
|
}
|
|
15918
16545
|
} else if (reason === "focus_leave") {
|
|
@@ -16190,16 +16817,36 @@ const openCallout = (
|
|
|
16190
16817
|
}
|
|
16191
16818
|
return openerElement === target || openerElement.contains(target);
|
|
16192
16819
|
};
|
|
16820
|
+
// Whether an outside press on `target` is spent on closing the callout
|
|
16821
|
+
// (`backdrop`). What is not the page behind the callout keeps its press: a
|
|
16822
|
+
// layer the callout is not part of — its own page may well be a sheet, a
|
|
16823
|
+
// sheet its content opened is in front of it — and a peer, which the press
|
|
16824
|
+
// is meant to open.
|
|
16825
|
+
const spendsPressOn = (target) => {
|
|
16826
|
+
if (!backdrop || !pressOutsideCloses()) {
|
|
16827
|
+
return false;
|
|
16828
|
+
}
|
|
16829
|
+
const layer = target.closest?.(LAYER_SELECTOR);
|
|
16830
|
+
if (layer && !layer.contains(calloutElement)) {
|
|
16831
|
+
return false;
|
|
16832
|
+
}
|
|
16833
|
+
if (findPopupPeer(target, group)) {
|
|
16834
|
+
return false;
|
|
16835
|
+
}
|
|
16836
|
+
return true;
|
|
16837
|
+
};
|
|
16838
|
+
const spend = (event) => {
|
|
16839
|
+
debug(event, `outside press spent on closing the callout (backdrop)`);
|
|
16840
|
+
event.preventDefault();
|
|
16841
|
+
event.stopPropagation();
|
|
16842
|
+
};
|
|
16193
16843
|
const handleClickOutside = (event) => {
|
|
16194
16844
|
if (event.button !== 0) {
|
|
16195
16845
|
// right click
|
|
16196
16846
|
return;
|
|
16197
16847
|
}
|
|
16198
16848
|
const clickTarget = event.target;
|
|
16199
|
-
if (
|
|
16200
|
-
clickTarget === calloutElement ||
|
|
16201
|
-
calloutElement.contains(clickTarget)
|
|
16202
|
-
) {
|
|
16849
|
+
if (calloutElement.contains(clickTarget)) {
|
|
16203
16850
|
return;
|
|
16204
16851
|
}
|
|
16205
16852
|
if (isInsideOpener(clickTarget)) {
|
|
@@ -16212,14 +16859,20 @@ const openCallout = (
|
|
|
16212
16859
|
debug(event, `click on opener, let its handler decide`);
|
|
16213
16860
|
return;
|
|
16214
16861
|
}
|
|
16862
|
+
// Read before the close, which takes the callout out of the document —
|
|
16863
|
+
// and out of the layer it stands in, for spendsPressOn to find.
|
|
16864
|
+
const spent = spendsPressOn(clickTarget);
|
|
16215
16865
|
requestClose(event, "click_outside");
|
|
16866
|
+
if (spent) {
|
|
16867
|
+
spend(event);
|
|
16868
|
+
}
|
|
16216
16869
|
};
|
|
16217
16870
|
const handleSpaceOutside = (event) => {
|
|
16218
16871
|
if (event.key !== " ") {
|
|
16219
16872
|
return;
|
|
16220
16873
|
}
|
|
16221
16874
|
const keyTarget = event.target;
|
|
16222
|
-
if (
|
|
16875
|
+
if (calloutElement.contains(keyTarget)) {
|
|
16223
16876
|
return;
|
|
16224
16877
|
}
|
|
16225
16878
|
if (isInsideOpener(keyTarget)) {
|
|
@@ -16228,7 +16881,41 @@ const openCallout = (
|
|
|
16228
16881
|
debug(event, `space on opener, let its handler decide`);
|
|
16229
16882
|
return;
|
|
16230
16883
|
}
|
|
16884
|
+
const spent = spendsPressOn(keyTarget);
|
|
16231
16885
|
requestClose(event, "space_outside");
|
|
16886
|
+
if (spent) {
|
|
16887
|
+
spend(event);
|
|
16888
|
+
}
|
|
16889
|
+
};
|
|
16890
|
+
// The press before its click: a click ends a gesture whose pointerdown
|
|
16891
|
+
// already reached the page, and what acts on the press itself — a hold, a
|
|
16892
|
+
// drag, the focus moving — would otherwise happen under a callout that only
|
|
16893
|
+
// closes on the click. Decided at the pointerdown and kept for the whole
|
|
16894
|
+
// press, wherever its later events land: a release beside the callout ends
|
|
16895
|
+
// a drag started inside it, and must reach it.
|
|
16896
|
+
let pressSpent = false;
|
|
16897
|
+
const handlePressOutside = (event) => {
|
|
16898
|
+
if (event.type === "pointerdown") {
|
|
16899
|
+
const pressTarget = event.target;
|
|
16900
|
+
pressSpent =
|
|
16901
|
+
event.button === 0 &&
|
|
16902
|
+
!calloutElement.contains(pressTarget) &&
|
|
16903
|
+
!isInsideOpener(pressTarget) &&
|
|
16904
|
+
spendsPressOn(pressTarget);
|
|
16905
|
+
} else if (event.type === "pointercancel") {
|
|
16906
|
+
// The browser took the press over — a scroll — and nothing follows it.
|
|
16907
|
+
pressSpent = false;
|
|
16908
|
+
return;
|
|
16909
|
+
}
|
|
16910
|
+
if (!pressSpent) {
|
|
16911
|
+
return;
|
|
16912
|
+
}
|
|
16913
|
+
if (event.type === "mousedown" || event.type === "contextmenu") {
|
|
16914
|
+
// What the press does on its own: the focus moving, a selection
|
|
16915
|
+
// starting, the menu of a held finger.
|
|
16916
|
+
event.preventDefault();
|
|
16917
|
+
}
|
|
16918
|
+
event.stopPropagation();
|
|
16232
16919
|
};
|
|
16233
16920
|
|
|
16234
16921
|
const registerClickOutsideListener = () => {
|
|
@@ -16238,6 +16925,16 @@ const openCallout = (
|
|
|
16238
16925
|
document.removeEventListener("click", handleClickOutside, true);
|
|
16239
16926
|
document.removeEventListener("keydown", handleSpaceOutside, true);
|
|
16240
16927
|
});
|
|
16928
|
+
if (backdrop) {
|
|
16929
|
+
for (const type of SPENT_PRESS_EVENT_TYPES) {
|
|
16930
|
+
document.addEventListener(type, handlePressOutside, true);
|
|
16931
|
+
}
|
|
16932
|
+
addTeardown(() => {
|
|
16933
|
+
for (const type of SPENT_PRESS_EVENT_TYPES) {
|
|
16934
|
+
document.removeEventListener(type, handlePressOutside, true);
|
|
16935
|
+
}
|
|
16936
|
+
});
|
|
16937
|
+
}
|
|
16241
16938
|
};
|
|
16242
16939
|
// A callout opened during a press (mousedown or pointerdown) must wait for the
|
|
16243
16940
|
// matching release before listening for click-outside, otherwise the same
|
|
@@ -17351,7 +18048,7 @@ const generateSvgWithoutArrow = (width, height) => {
|
|
|
17351
18048
|
*
|
|
17352
18049
|
* Usage:
|
|
17353
18050
|
* const myToken = createOpenToken();
|
|
17354
|
-
* calloutManager.addOpenToken(myToken, { message, status, testId, closeByScroll, anchorElement, event, skipFocus, onClose });
|
|
18051
|
+
* calloutManager.addOpenToken(myToken, { message, status, testId, closeByScroll, backdrop, group, anchorElement, event, skipFocus, onClose });
|
|
17355
18052
|
* calloutManager.removeOpenToken(myToken, event);
|
|
17356
18053
|
* calloutManager.requestCloseCallout(event, debugReason); // force-close all
|
|
17357
18054
|
* calloutManager.callout // current open callout or null
|
|
@@ -17409,6 +18106,8 @@ const createCalloutManager = (
|
|
|
17409
18106
|
closeButton: tokenData.closeButton,
|
|
17410
18107
|
closeByPressOutside: tokenData.status !== "error",
|
|
17411
18108
|
closeByScroll: tokenData.closeByScroll,
|
|
18109
|
+
backdrop: tokenData.backdrop,
|
|
18110
|
+
group: tokenData.group,
|
|
17412
18111
|
anchorElement,
|
|
17413
18112
|
openingEvent: event,
|
|
17414
18113
|
skipFocus: tokenData.skipFocus,
|
|
@@ -17537,6 +18236,8 @@ const createCalloutManager = (
|
|
|
17537
18236
|
icon,
|
|
17538
18237
|
closeButton,
|
|
17539
18238
|
closeByScroll,
|
|
18239
|
+
backdrop,
|
|
18240
|
+
group,
|
|
17540
18241
|
anchorElement,
|
|
17541
18242
|
event,
|
|
17542
18243
|
skipFocus,
|
|
@@ -17554,6 +18255,8 @@ const createCalloutManager = (
|
|
|
17554
18255
|
icon,
|
|
17555
18256
|
closeButton,
|
|
17556
18257
|
closeByScroll,
|
|
18258
|
+
backdrop,
|
|
18259
|
+
group,
|
|
17557
18260
|
skipFocus,
|
|
17558
18261
|
onClose,
|
|
17559
18262
|
// Resolved as the token is added, and kept with it: a token shown later
|
|
@@ -23644,7 +24347,16 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
23644
24347
|
}
|
|
23645
24348
|
return itemSignal;
|
|
23646
24349
|
};
|
|
23647
|
-
|
|
24350
|
+
// A value changes a row when it is another object — the app writing a new
|
|
24351
|
+
// one says something changed, and that is taken as said. An answer says it
|
|
24352
|
+
// with every object nested in its rows: JSON.parse makes them all new, the
|
|
24353
|
+
// ones that did not move included, so a revalidation returning the same
|
|
24354
|
+
// rows would replace every one of them for every reader. What an answer
|
|
24355
|
+
// sends again equal in depth is the value already held (see upsertAnswer).
|
|
24356
|
+
const valueChangedByWrite = (newValue, oldValue) => newValue !== oldValue;
|
|
24357
|
+
const valueChangedByAnswer = (newValue, oldValue) =>
|
|
24358
|
+
newValue !== oldValue && !compareTwoJsValues(newValue, oldValue);
|
|
24359
|
+
const upsertWith = (args, valueChanged) => {
|
|
23648
24360
|
const mutationsMap = new Map(); // Map<itemId, propertyMutations>
|
|
23649
24361
|
const triggerPropertyMutations = () => {
|
|
23650
24362
|
for (const itemPropertiesObserver of itemPropertiesObserverSet) {
|
|
@@ -23708,7 +24420,27 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
23708
24420
|
const newValue = props[key];
|
|
23709
24421
|
if (itemOwnKeys.includes(key)) {
|
|
23710
24422
|
const oldValue = item[key];
|
|
23711
|
-
|
|
24423
|
+
const { set } = itemOwnPropertyDescriptors[key];
|
|
24424
|
+
if (set) {
|
|
24425
|
+
// A relation (.one(), .many() and their scoped forms in
|
|
24426
|
+
// resource_graph.js): the row holds which children it points to,
|
|
24427
|
+
// the children live in their own store, and whoever reads them
|
|
24428
|
+
// follows them there. Its setter writes them and says whether the
|
|
24429
|
+
// row points elsewhere now. Compared as a value instead, the
|
|
24430
|
+
// children received are never the object the row hands out, and
|
|
24431
|
+
// every write naming the relation would replace the row.
|
|
24432
|
+
if (set.call(itemWithProps, newValue)) {
|
|
24433
|
+
hasChanges = true;
|
|
24434
|
+
propertyMutations[key] = {
|
|
24435
|
+
oldValue,
|
|
24436
|
+
newValue,
|
|
24437
|
+
target: item,
|
|
24438
|
+
newTarget: itemWithProps,
|
|
24439
|
+
};
|
|
24440
|
+
}
|
|
24441
|
+
continue;
|
|
24442
|
+
}
|
|
24443
|
+
if (valueChanged(newValue, oldValue)) {
|
|
23712
24444
|
hasChanges = true;
|
|
23713
24445
|
itemWithProps[key] = newValue;
|
|
23714
24446
|
propertyMutations[key] = {
|
|
@@ -23860,6 +24592,9 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
23860
24592
|
triggerPropertyMutations();
|
|
23861
24593
|
return item;
|
|
23862
24594
|
};
|
|
24595
|
+
const upsert = (...args) => upsertWith(args, valueChangedByWrite);
|
|
24596
|
+
// The same, for the result of a callback — what the server answered.
|
|
24597
|
+
const upsertAnswer = (...args) => upsertWith(args, valueChangedByAnswer);
|
|
23863
24598
|
const drop = (...args) => {
|
|
23864
24599
|
const removedItemArray = [];
|
|
23865
24600
|
const triggerRemovedMutations = () => {
|
|
@@ -24036,6 +24771,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
24036
24771
|
selectAll,
|
|
24037
24772
|
itemSignalForId,
|
|
24038
24773
|
upsert,
|
|
24774
|
+
upsertAnswer,
|
|
24039
24775
|
drop,
|
|
24040
24776
|
|
|
24041
24777
|
observeItemProperties,
|
|
@@ -24793,7 +25529,7 @@ const createRangeReader = (
|
|
|
24793
25529
|
`${actionName} must return { items, start, count }, received ${describeResult(result)}.`,
|
|
24794
25530
|
);
|
|
24795
25531
|
}
|
|
24796
|
-
const items = store.
|
|
25532
|
+
const items = store.upsertAnswer(result.items);
|
|
24797
25533
|
let { start, count } = result;
|
|
24798
25534
|
if (start === undefined) {
|
|
24799
25535
|
const startAsked = rangeParams.start;
|
|
@@ -25454,7 +26190,7 @@ const createResource = (
|
|
|
25454
26190
|
: // GET/PUT contract (see .one() JSDoc): the parent object with the
|
|
25455
26191
|
// relationship nested inside, or null for no relationship.
|
|
25456
26192
|
(result) => {
|
|
25457
|
-
const item = store.
|
|
26193
|
+
const item = store.upsertAnswer(result);
|
|
25458
26194
|
const childItem = item[propertyName];
|
|
25459
26195
|
return childItem ? childItem[childIdKey] : undefined;
|
|
25460
26196
|
};
|
|
@@ -25635,8 +26371,8 @@ const createResource = (
|
|
|
25635
26371
|
: (childData) => {
|
|
25636
26372
|
// an array is [property, value, props], used to rename the child id
|
|
25637
26373
|
const childItem = Array.isArray(childData)
|
|
25638
|
-
? childStore.
|
|
25639
|
-
: childStore.
|
|
26374
|
+
? childStore.upsertAnswer(...childData)
|
|
26375
|
+
: childStore.upsertAnswer(childData);
|
|
25640
26376
|
return childItem[childIdKey];
|
|
25641
26377
|
};
|
|
25642
26378
|
const throwInvalidResult = createInvalidResultThrower(
|
|
@@ -25678,7 +26414,7 @@ const createResource = (
|
|
|
25678
26414
|
? (result) => {
|
|
25679
26415
|
// GET_MANY contract (see .many() JSDoc): the parent object with
|
|
25680
26416
|
// the child array nested inside; the array replaces the relationship.
|
|
25681
|
-
const item = store.
|
|
26417
|
+
const item = store.upsertAnswer(result);
|
|
25682
26418
|
const childItemArray = item[propertyName];
|
|
25683
26419
|
return childItemArray.map((childItem) => childItem[childIdKey]);
|
|
25684
26420
|
}
|
|
@@ -25709,7 +26445,7 @@ const createResource = (
|
|
|
25709
26445
|
return deletedChildItemIdArray;
|
|
25710
26446
|
}
|
|
25711
26447
|
: (childDataArray) => {
|
|
25712
|
-
const childItemArray = childStore.
|
|
26448
|
+
const childItemArray = childStore.upsertAnswer(childDataArray);
|
|
25713
26449
|
return childItemArray.map((childItem) => childItem[childIdKey]);
|
|
25714
26450
|
};
|
|
25715
26451
|
const throwInvalidResult = createInvalidResultThrower(
|
|
@@ -25848,18 +26584,37 @@ const createResource = (
|
|
|
25848
26584
|
childSetup(childItem);
|
|
25849
26585
|
}
|
|
25850
26586
|
const childSignal = signal(null);
|
|
26587
|
+
// Returns whether the child changed — what the store asks a relation
|
|
26588
|
+
// (see assign in array_signal_store.js). Assigned in place, the child
|
|
26589
|
+
// tells nobody itself: a prop it receives changed is a change of the
|
|
26590
|
+
// owner's row, which is how its readers see it.
|
|
25851
26591
|
const applyProps = (props) => {
|
|
25852
26592
|
if (!props) {
|
|
26593
|
+
if (childSignal.peek() === null) {
|
|
26594
|
+
return false;
|
|
26595
|
+
}
|
|
25853
26596
|
childSignal.value = null;
|
|
25854
|
-
return;
|
|
26597
|
+
return true;
|
|
25855
26598
|
}
|
|
25856
|
-
|
|
26599
|
+
let changed = false;
|
|
26600
|
+
// Assign each prop in place. Reactive setters (from chained .one()
|
|
26601
|
+
// etc.) fire, and say whether their relation changed.
|
|
25857
26602
|
for (const [key, value] of Object.entries(props)) {
|
|
25858
|
-
|
|
26603
|
+
const descriptor = Object.getOwnPropertyDescriptor(childItem, key);
|
|
26604
|
+
if (descriptor && descriptor.set) {
|
|
26605
|
+
if (descriptor.set.call(childItem, value)) {
|
|
26606
|
+
changed = true;
|
|
26607
|
+
}
|
|
26608
|
+
} else if (childItem[key] !== value) {
|
|
26609
|
+
childItem[key] = value;
|
|
26610
|
+
changed = true;
|
|
26611
|
+
}
|
|
25859
26612
|
}
|
|
25860
26613
|
if (childSignal.peek() !== childItem) {
|
|
25861
26614
|
childSignal.value = childItem; // first activation: null → childItem
|
|
26615
|
+
changed = true;
|
|
25862
26616
|
}
|
|
26617
|
+
return changed;
|
|
25863
26618
|
};
|
|
25864
26619
|
if (!ownerName) {
|
|
25865
26620
|
// Declared on a scoped child, an owner id is only unique inside its
|
|
@@ -26147,7 +26902,7 @@ const createResource = (
|
|
|
26147
26902
|
if (isMany) {
|
|
26148
26903
|
// GET_MANY, POST_MANY, PUT_MANY etc: rest[0] is the array of items,
|
|
26149
26904
|
// and it replaces the whole collection.
|
|
26150
|
-
const itemArray = childStore.
|
|
26905
|
+
const itemArray = childStore.upsertAnswer(rest[0]);
|
|
26151
26906
|
const idArray = itemArray.map((childItem) => childItem[childIdKey]);
|
|
26152
26907
|
idArraySignal.value = idArray;
|
|
26153
26908
|
return [ownerId, idArray];
|
|
@@ -26156,8 +26911,8 @@ const createResource = (
|
|
|
26156
26911
|
// GET, POST, PUT, PATCH: rest may be [props] or [oldId, props] for renames
|
|
26157
26912
|
const childItem =
|
|
26158
26913
|
rest.length > 1
|
|
26159
|
-
? childStore.
|
|
26160
|
-
: childStore.
|
|
26914
|
+
? childStore.upsertAnswer(...rest)
|
|
26915
|
+
: childStore.upsertAnswer(rest[0]);
|
|
26161
26916
|
return [ownerId, childItem[childIdKey]];
|
|
26162
26917
|
},
|
|
26163
26918
|
valueToData: (value) => {
|
|
@@ -26293,8 +27048,8 @@ const createRestActionFactoryForRoot = (
|
|
|
26293
27048
|
// An array result is [property, value, props] — used to rename the
|
|
26294
27049
|
// idKey of an item: store.upsert("name", "currentName", { name: "newName" })
|
|
26295
27050
|
const item = Array.isArray(result)
|
|
26296
|
-
? store.
|
|
26297
|
-
: store.
|
|
27051
|
+
? store.upsertAnswer(...result)
|
|
27052
|
+
: store.upsertAnswer(result);
|
|
26298
27053
|
return item[idKey];
|
|
26299
27054
|
};
|
|
26300
27055
|
const throwInvalidResult = createInvalidResultThrower(
|
|
@@ -26358,7 +27113,7 @@ const createRestActionFactoryForRoot = (
|
|
|
26358
27113
|
verb === "DELETE"
|
|
26359
27114
|
? (idOrMutableIdArray) => store.drop(idOrMutableIdArray)
|
|
26360
27115
|
: (dataArray) => {
|
|
26361
|
-
const itemArray = store.
|
|
27116
|
+
const itemArray = store.upsertAnswer(dataArray);
|
|
26362
27117
|
return itemArray.map((item) => item[idKey]);
|
|
26363
27118
|
};
|
|
26364
27119
|
|
|
@@ -26487,14 +27242,17 @@ ${relationLabel} source location: ${declarationSite}`,
|
|
|
26487
27242
|
// array of child props/ids, or anything else meaning "empty") into an array of
|
|
26488
27243
|
// child ids, upserting each entry into the child store. The id array signal is
|
|
26489
27244
|
// only touched when the resulting ids actually differ.
|
|
27245
|
+
// Returns whether the children the row points to changed — what the store asks
|
|
27246
|
+
// a relation (see assign in array_signal_store.js).
|
|
26490
27247
|
const createChildIdArrayUpdater = (childStore, childIdKey, idArraySignal) => {
|
|
26491
27248
|
return (valueArray) => {
|
|
26492
27249
|
const currentIdArray = idArraySignal.peek();
|
|
26493
27250
|
if (!Array.isArray(valueArray)) {
|
|
26494
27251
|
if (currentIdArray.length > 0) {
|
|
26495
27252
|
idArraySignal.value = [];
|
|
27253
|
+
return true;
|
|
26496
27254
|
}
|
|
26497
|
-
return;
|
|
27255
|
+
return false;
|
|
26498
27256
|
}
|
|
26499
27257
|
const idArray = [];
|
|
26500
27258
|
let modified = false;
|
|
@@ -26516,7 +27274,9 @@ const createChildIdArrayUpdater = (childStore, childIdKey, idArraySignal) => {
|
|
|
26516
27274
|
}
|
|
26517
27275
|
if (modified || currentIdArray.length !== idArray.length) {
|
|
26518
27276
|
idArraySignal.value = idArray;
|
|
27277
|
+
return true;
|
|
26519
27278
|
}
|
|
27279
|
+
return false;
|
|
26520
27280
|
};
|
|
26521
27281
|
};
|
|
26522
27282
|
|
|
@@ -26934,6 +27694,24 @@ const TYPE_CONVERTERS = {
|
|
|
26934
27694
|
},
|
|
26935
27695
|
};
|
|
26936
27696
|
|
|
27697
|
+
/**
|
|
27698
|
+
* What a component can learn about the page it is rendered in, from the route
|
|
27699
|
+
* container rendering that page (see useContentKeepingLeavingPages in
|
|
27700
|
+
* route.jsx). `null` outside of any route container.
|
|
27701
|
+
*
|
|
27702
|
+
* - `isShown()` — whether the container would still show the page. False as
|
|
27703
|
+
* soon as the address leads elsewhere: the page is about to be taken down, or
|
|
27704
|
+
* hidden until the route transition leaving it is over, and nothing it reads
|
|
27705
|
+
* is worth a render any more. False for good once it is kept while leaving: a
|
|
27706
|
+
* return to its route mounts the page anew. Asked outside any render — by a
|
|
27707
|
+
* subscription deciding whether to re-render — and it reads the routes
|
|
27708
|
+
* untracked, so asking subscribes nobody to them.
|
|
27709
|
+
* - `leavingSignal` — true once the page is hidden and kept while its movement
|
|
27710
|
+
* plays: still mounted, so what it fills outside its own nodes (a slot in a
|
|
27711
|
+
* bar) steps out rather than showing with the page arriving.
|
|
27712
|
+
*/
|
|
27713
|
+
const RoutePageContext = createContext(null);
|
|
27714
|
+
|
|
26937
27715
|
const promiseStateWeakMap = new WeakMap();
|
|
26938
27716
|
const usePromiseAsyncData = (
|
|
26939
27717
|
promise,
|
|
@@ -27069,12 +27847,39 @@ const useActionAsyncData = (action, {
|
|
|
27069
27847
|
// has already processed the settlement and the detached DOM is discarded.
|
|
27070
27848
|
const runningState = action.runningStateSignal.peek();
|
|
27071
27849
|
const [, setTick] = useState(0);
|
|
27850
|
+
const routePage = useContext(RoutePageContext);
|
|
27072
27851
|
useEffect(() => {
|
|
27852
|
+
let stopOwingRender = null;
|
|
27853
|
+
const rerender = () => {
|
|
27854
|
+
// Not for a page the address has left: it is about to be taken down,
|
|
27855
|
+
// or hidden until the route transition leaving it is over, and its
|
|
27856
|
+
// route's action moving as that route stops matching would re-render
|
|
27857
|
+
// the whole page — inside the transition's update callback, where
|
|
27858
|
+
// every render delays the movement (see route_page_context.js).
|
|
27859
|
+
if (routePage && !routePage.isShown()) {
|
|
27860
|
+
// Owed rather than dropped: the address can come back before the
|
|
27861
|
+
// container renders (a redirect straight back), and the page then
|
|
27862
|
+
// stays on screen with what it read last.
|
|
27863
|
+
if (!stopOwingRender) {
|
|
27864
|
+
stopOwingRender = observeRouteRender(() => {
|
|
27865
|
+
if (routePage.isShown()) {
|
|
27866
|
+
rerender();
|
|
27867
|
+
}
|
|
27868
|
+
});
|
|
27869
|
+
}
|
|
27870
|
+
return;
|
|
27871
|
+
}
|
|
27872
|
+
if (stopOwingRender) {
|
|
27873
|
+
stopOwingRender();
|
|
27874
|
+
stopOwingRender = null;
|
|
27875
|
+
}
|
|
27876
|
+
setTick(n => n + 1);
|
|
27877
|
+
};
|
|
27073
27878
|
const unsubscribeFromRunningState = action.runningStateSignal.subscribe(state => {
|
|
27074
27879
|
if (state === RUNNING) {
|
|
27075
27880
|
dismissedActionWeakSet.delete(action);
|
|
27076
27881
|
}
|
|
27077
|
-
|
|
27882
|
+
rerender();
|
|
27078
27883
|
});
|
|
27079
27884
|
// The data does not come from this action's runs alone: dataSignal is a
|
|
27080
27885
|
// computed over the resource store, so an other action writing that store
|
|
@@ -27088,7 +27893,7 @@ const useActionAsyncData = (action, {
|
|
|
27088
27893
|
dataNotificationIsInitial = false;
|
|
27089
27894
|
return;
|
|
27090
27895
|
}
|
|
27091
|
-
|
|
27896
|
+
rerender();
|
|
27092
27897
|
});
|
|
27093
27898
|
// The params say WHICH question this is, and this hook reads them: to know
|
|
27094
27899
|
// there is nothing to ask for, and to start the run it owns. A binding
|
|
@@ -27101,7 +27906,7 @@ const useActionAsyncData = (action, {
|
|
|
27101
27906
|
paramsNotificationIsInitial = false;
|
|
27102
27907
|
return;
|
|
27103
27908
|
}
|
|
27104
|
-
|
|
27909
|
+
rerender();
|
|
27105
27910
|
});
|
|
27106
27911
|
// A debounced binding waits before it retargets, so nothing above changes
|
|
27107
27912
|
// while the delay runs — but what is on screen is already out of date.
|
|
@@ -27111,18 +27916,21 @@ const useActionAsyncData = (action, {
|
|
|
27111
27916
|
settlingNotificationIsInitial = false;
|
|
27112
27917
|
return;
|
|
27113
27918
|
}
|
|
27114
|
-
|
|
27919
|
+
rerender();
|
|
27115
27920
|
});
|
|
27116
27921
|
return () => {
|
|
27117
27922
|
unsubscribeFromRunningState();
|
|
27118
27923
|
unsubscribeFromData();
|
|
27119
27924
|
unsubscribeFromParams();
|
|
27120
27925
|
unsubscribeFromParamsSettling();
|
|
27926
|
+
if (stopOwingRender) {
|
|
27927
|
+
stopOwingRender();
|
|
27928
|
+
}
|
|
27121
27929
|
};
|
|
27122
27930
|
// Bound to the action, not to the mount: params given as a plain object
|
|
27123
27931
|
// make another action instance, and the component would otherwise stay
|
|
27124
27932
|
// subscribed to the state of the one it no longer reads.
|
|
27125
|
-
}, [action]);
|
|
27933
|
+
}, [action, routePage]);
|
|
27126
27934
|
|
|
27127
27935
|
// The params moved and the run has not started: under a debounce the action
|
|
27128
27936
|
// is still the previous one, COMPLETED with the previous answer. There is no
|
|
@@ -31223,13 +32031,6 @@ const keepLeavingPages = () => {
|
|
|
31223
32031
|
hold.containers.clear();
|
|
31224
32032
|
};
|
|
31225
32033
|
};
|
|
31226
|
-
|
|
31227
|
-
// What a page can learn about its place from the container rendering it:
|
|
31228
|
-
// whether it is a page kept while leaving (see keepLeavingPages). Such a page
|
|
31229
|
-
// is still mounted, and whatever it fills OUTSIDE its own nodes — a slot in a
|
|
31230
|
-
// bar, say — would otherwise go on showing with the page arriving (see
|
|
31231
|
-
// layout/slot.jsx).
|
|
31232
|
-
const RoutePageContext = createContext(null);
|
|
31233
32034
|
const LEAVING_ATTRIBUTE = "data-navi-route-leaving";
|
|
31234
32035
|
const css$13 = /* css */`[data-navi-route-leaving] {
|
|
31235
32036
|
display: none !important;
|
|
@@ -31354,7 +32155,21 @@ const RouteContainer = ({
|
|
|
31354
32155
|
shownBranchRef.current = activeBranch;
|
|
31355
32156
|
}
|
|
31356
32157
|
const branch = shownBranchRef.current || activeBranch;
|
|
31357
|
-
|
|
32158
|
+
// What this container would show if it rendered now, by the same rule as
|
|
32159
|
+
// the render above — asked from outside any render (see isShown below).
|
|
32160
|
+
const childrenRef = useRef(null);
|
|
32161
|
+
childrenRef.current = children;
|
|
32162
|
+
const readShownBranchRef = useRef(null);
|
|
32163
|
+
if (!readShownBranchRef.current) {
|
|
32164
|
+
readShownBranchRef.current = () => {
|
|
32165
|
+
if (routeRenderFrozen) {
|
|
32166
|
+
return shownBranchRef.current;
|
|
32167
|
+
}
|
|
32168
|
+
return collectBranches(childrenRef.current).activeBranch;
|
|
32169
|
+
};
|
|
32170
|
+
}
|
|
32171
|
+
const parentPage = useContext(RoutePageContext);
|
|
32172
|
+
const content = useContentKeepingLeavingPages(branch, readShownBranchRef.current, parentPage);
|
|
31358
32173
|
|
|
31359
32174
|
// The two things this component knows that nobody outside can find out: the
|
|
31360
32175
|
// pages a named fallback is the absence of — the children it was written
|
|
@@ -31391,7 +32206,7 @@ const RouteContainer = ({
|
|
|
31391
32206
|
// page being left must stay the child it was when a page is added beside it,
|
|
31392
32207
|
// or Preact takes it down and builds it again.
|
|
31393
32208
|
let leavingKeyCount = 0;
|
|
31394
|
-
const useContentKeepingLeavingPages = branch => {
|
|
32209
|
+
const useContentKeepingLeavingPages = (branch, readShownBranch, parentPage) => {
|
|
31395
32210
|
const [, rerender] = useReducer(countRenders, 0);
|
|
31396
32211
|
const shownRef = useRef(null);
|
|
31397
32212
|
const leavingRef = useRef([]);
|
|
@@ -31409,6 +32224,7 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
31409
32224
|
// of reach — the page arriving could not even take the focus — for as long
|
|
31410
32225
|
// as it is open, and one in a page kept while leaving stays open for the
|
|
31411
32226
|
// whole movement. The page is then taken down at once, as without one.
|
|
32227
|
+
shown.page.left = true;
|
|
31412
32228
|
leavingRef.current.push({
|
|
31413
32229
|
vnode: shown.vnode,
|
|
31414
32230
|
page: shown.page,
|
|
@@ -31425,10 +32241,11 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
31425
32241
|
let vnode = null;
|
|
31426
32242
|
if (branch) {
|
|
31427
32243
|
if (!page) {
|
|
31428
|
-
page =
|
|
31429
|
-
leavingSignal: signal(false)
|
|
31430
|
-
};
|
|
32244
|
+
page = createRoutePage(readShownBranch, parentPage);
|
|
31431
32245
|
}
|
|
32246
|
+
// A page carried over follows the branch it is now the page of.
|
|
32247
|
+
page.branchId = branch.id;
|
|
32248
|
+
page.element = element;
|
|
31432
32249
|
vnode = h(RoutePageContext.Provider, {
|
|
31433
32250
|
key,
|
|
31434
32251
|
value: page
|
|
@@ -31495,6 +32312,33 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
31495
32312
|
return children;
|
|
31496
32313
|
};
|
|
31497
32314
|
const countRenders = count => count + 1;
|
|
32315
|
+
// The page a container renders, as RoutePageContext hands it over (see
|
|
32316
|
+
// route_page_context.js). Shown while the container would pick its branch, or
|
|
32317
|
+
// a branch rendering the same element (it is then carried over), and while the
|
|
32318
|
+
// page it sits in is shown itself. Never again once kept while leaving: coming
|
|
32319
|
+
// back to its route mounts the page anew.
|
|
32320
|
+
const createRoutePage = (readShownBranch, parentPage) => {
|
|
32321
|
+
const page = {
|
|
32322
|
+
branchId: null,
|
|
32323
|
+
element: undefined,
|
|
32324
|
+
left: false,
|
|
32325
|
+
leavingSignal: signal(false),
|
|
32326
|
+
isShown: () => {
|
|
32327
|
+
if (page.left) {
|
|
32328
|
+
return false;
|
|
32329
|
+
}
|
|
32330
|
+
if (parentPage && !parentPage.isShown()) {
|
|
32331
|
+
return false;
|
|
32332
|
+
}
|
|
32333
|
+
const shownBranch = untracked(readShownBranch);
|
|
32334
|
+
if (!shownBranch) {
|
|
32335
|
+
return false;
|
|
32336
|
+
}
|
|
32337
|
+
return shownBranch.id === page.branchId || branchElement(shownBranch) === page.element;
|
|
32338
|
+
}
|
|
32339
|
+
};
|
|
32340
|
+
return page;
|
|
32341
|
+
};
|
|
31498
32342
|
// What Preact compares when a container changes page, below the <Route> it
|
|
31499
32343
|
// renders either way: the element the page is rendered with — a layout, for a
|
|
31500
32344
|
// container branch.
|
|
@@ -37758,834 +38602,336 @@ registerNaviCommand("--navi-nav-to", (source, event, { argument }) => {
|
|
|
37758
38602
|
};
|
|
37759
38603
|
});
|
|
37760
38604
|
|
|
37761
|
-
// Back to the screen the reader came from, with somewhere to land when there
|
|
37762
|
-
// is none of ours behind — a url opened cold (see navBack). The fallback is
|
|
37763
|
-
// the argument for the reason --navi-nav-to's destination is: it says WHAT the
|
|
37764
|
-
// command does when there is nothing to go back to.
|
|
37765
|
-
registerNaviCommand("--navi-nav-back", (source, event, { argument }) => {
|
|
37766
|
-
const target = resolveExplicitTarget(source) || source;
|
|
37767
|
-
return {
|
|
37768
|
-
target,
|
|
37769
|
-
implementation: () => navBack({ fallback: argument }),
|
|
37770
|
-
};
|
|
37771
|
-
});
|
|
37772
|
-
|
|
37773
|
-
registerNaviCommand(
|
|
37774
|
-
"--navi-toggle",
|
|
37775
|
-
(source, event, { anchor, value } = {}) => {
|
|
37776
|
-
const target =
|
|
37777
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
37778
|
-
if (!target) {
|
|
37779
|
-
return undefined;
|
|
37780
|
-
}
|
|
37781
|
-
return {
|
|
37782
|
-
target,
|
|
37783
|
-
implementation: () => {
|
|
37784
|
-
const isExpanded = target.getAttribute("aria-expanded") === "true";
|
|
37785
|
-
const customEventName = isExpanded
|
|
37786
|
-
? "navi_request_close"
|
|
37787
|
-
: "navi_request_open";
|
|
37788
|
-
return dispatchCustomEvent(target, customEventName, {
|
|
37789
|
-
event,
|
|
37790
|
-
source: resolveCommandProxySource(source),
|
|
37791
|
-
anchor,
|
|
37792
|
-
// Same as --navi-open below: the half of the toggle that opens says
|
|
37793
|
-
// what it opens ON, and the half that closes carries it too so the
|
|
37794
|
-
// popup never has to ask which half it just heard.
|
|
37795
|
-
value:
|
|
37796
|
-
value === undefined ? resolveCommandValue(source, event) : value,
|
|
37797
|
-
});
|
|
37798
|
-
},
|
|
37799
|
-
};
|
|
37800
|
-
},
|
|
37801
|
-
);
|
|
37802
|
-
// A popup that edits opens ON something, and the press is the only place that
|
|
37803
|
-
// knows which one. It says it with `value` — "what this is about", as
|
|
37804
|
-
// everywhere else — rather than with an argument, which says what the command
|
|
37805
|
-
// DOES: "open" is already a complete instruction, unlike --navi-go-to-slide.
|
|
37806
|
-
// <Button value={radar.id} command="--navi-open" commandfor="radar-dialog">
|
|
37807
|
-
// The popup is told before it opens — see Dialog/Popover's `onOpen`.
|
|
37808
|
-
registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
|
|
37809
|
-
const target =
|
|
37810
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
37811
|
-
if (!target) {
|
|
37812
|
-
return undefined;
|
|
37813
|
-
}
|
|
37814
|
-
return {
|
|
37815
|
-
target,
|
|
37816
|
-
implementation: () => {
|
|
37817
|
-
// The popup reads `anchor` first and falls back to the source (see
|
|
37818
|
-
// onnavi_request_open in popover.jsx): without an `anchor`, the element
|
|
37819
|
-
// asking is the element opened against.
|
|
37820
|
-
return dispatchCustomEvent(target, "navi_request_open", {
|
|
37821
|
-
event,
|
|
37822
|
-
source: resolveCommandProxySource(source),
|
|
37823
|
-
anchor,
|
|
37824
|
-
value: value === undefined ? resolveCommandValue(source, event) : value,
|
|
37825
|
-
});
|
|
37826
|
-
},
|
|
37827
|
-
};
|
|
37828
|
-
});
|
|
37829
|
-
/**
|
|
37830
|
-
* Tells the popup a source opens that a press on it has started, ahead of the
|
|
37831
|
-
* release that runs the command: what the opening will read can be asked for
|
|
37832
|
-
* now (see open_controller.js). A press rather than a hover, because what
|
|
37833
|
-
* that reads is keyed by the popup — one sheet per card — and a pointer
|
|
37834
|
-
* crossing a list of cards has chosen none of them; on a touch screen the
|
|
37835
|
-
* press is the first thing the finger says anyway.
|
|
37836
|
-
*
|
|
37837
|
-
* The target is found the way --navi-open finds it, but silently: nothing
|
|
37838
|
-
* runs here, and a source with no target says so at the release.
|
|
37839
|
-
*/
|
|
37840
|
-
const announceOpeningPress = (source, command) => {
|
|
37841
|
-
if (command !== "--navi-open" && command !== "--navi-toggle") {
|
|
37842
|
-
return;
|
|
37843
|
-
}
|
|
37844
|
-
const commandFor = source.getAttribute("commandfor");
|
|
37845
|
-
const target = commandFor
|
|
37846
|
-
? document.getElementById(commandFor)
|
|
37847
|
-
: resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
37848
|
-
if (!target) {
|
|
37849
|
-
return;
|
|
37850
|
-
}
|
|
37851
|
-
dispatchCustomEvent(target, "navi_open_press");
|
|
37852
|
-
};
|
|
37853
|
-
// "--navi-close:all" closes every expandable above the source, nearest first —
|
|
37854
|
-
// a link leaving from a badge shown over a sheet leaves both. A surface that
|
|
37855
|
-
// refuses (a form asking about its changes) keeps what is above it open too:
|
|
37856
|
-
// one cannot be out of the sheet while still in the badge.
|
|
37857
|
-
registerNaviCommand("--navi-close", (source, event, { argument }) => {
|
|
37858
|
-
const target =
|
|
37859
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
37860
|
-
if (!target) {
|
|
37861
|
-
return undefined;
|
|
37862
|
-
}
|
|
37863
|
-
return {
|
|
37864
|
-
target,
|
|
37865
|
-
implementation: () => {
|
|
37866
|
-
const detail = { event, source: resolveCommandProxySource(source) };
|
|
37867
|
-
if (argument === "all") {
|
|
37868
|
-
return requestCloseUpward(target, detail);
|
|
37869
|
-
}
|
|
37870
|
-
return dispatchCustomEvent(target, "navi_request_close", detail);
|
|
37871
|
-
},
|
|
37872
|
-
};
|
|
37873
|
-
});
|
|
37874
|
-
const requestCloseUpward = (target, detail) => {
|
|
37875
|
-
let expandable = target;
|
|
37876
|
-
while (expandable) {
|
|
37877
|
-
const closing = dispatchCustomEvent(
|
|
37878
|
-
expandable,
|
|
37879
|
-
"navi_request_close",
|
|
37880
|
-
detail,
|
|
37881
|
-
);
|
|
37882
|
-
if (!closing) {
|
|
37883
|
-
return false;
|
|
37884
|
-
}
|
|
37885
|
-
const parent = expandable.parentElement;
|
|
37886
|
-
expandable = parent ? parent.closest("[aria-expanded]") : null;
|
|
37887
|
-
}
|
|
37888
|
-
return true;
|
|
37889
|
-
};
|
|
37890
|
-
registerNaviCommand("--navi-cancel", (source, event) => {
|
|
37891
|
-
const target =
|
|
37892
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
37893
|
-
if (!target) {
|
|
37894
|
-
return undefined;
|
|
37895
|
-
}
|
|
37896
|
-
return {
|
|
37897
|
-
target,
|
|
37898
|
-
implementation: () => {
|
|
37899
|
-
return dispatchCustomEvent(target, "navi_request_close", {
|
|
37900
|
-
event,
|
|
37901
|
-
source: resolveCommandProxySource(source),
|
|
37902
|
-
isCancel: true,
|
|
37903
|
-
});
|
|
37904
|
-
},
|
|
37905
|
-
};
|
|
37906
|
-
});
|
|
37907
|
-
// "Yes, do it" — said inside the popup of a confirm picker (see
|
|
37908
|
-
// picker/picker_confirm.jsx), by its own yes button or by whatever a caller put
|
|
37909
|
-
// there instead. Its counterpart is --navi-cancel; the difference between the
|
|
37910
|
-
// two is this event, since both then close the popup, and a popup that closes
|
|
37911
|
-
// without having said this is the answer "no".
|
|
37912
|
-
registerNaviCommand("--navi-confirm", (source, event) => {
|
|
37913
|
-
const target =
|
|
37914
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
37915
|
-
if (!target) {
|
|
37916
|
-
return undefined;
|
|
37917
|
-
}
|
|
37918
|
-
return {
|
|
37919
|
-
target,
|
|
37920
|
-
implementation: () => {
|
|
37921
|
-
const commandSource = resolveCommandProxySource(source);
|
|
37922
|
-
dispatchCustomEvent(target, "navi_request_confirm", {
|
|
37923
|
-
event,
|
|
37924
|
-
source: commandSource,
|
|
37925
|
-
});
|
|
37926
|
-
return dispatchCustomEvent(target, "navi_request_close", {
|
|
37927
|
-
event,
|
|
37928
|
-
source: commandSource,
|
|
37929
|
-
});
|
|
37930
|
-
},
|
|
37931
|
-
};
|
|
37932
|
-
});
|
|
37933
|
-
registerNaviCommand("--navi-define", (source, event) => {
|
|
37934
|
-
const target =
|
|
37935
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
37936
|
-
if (!target) {
|
|
37937
|
-
return undefined;
|
|
37938
|
-
}
|
|
37939
|
-
return {
|
|
37940
|
-
target,
|
|
37941
|
-
implementation: () => executeNaviDefine(source, event, target),
|
|
37942
|
-
};
|
|
37943
|
-
});
|
|
37944
|
-
// A send whose target is the popup itself: what is around the source IS the
|
|
37945
|
-
// popup's answer, and a picker runs its action with that answer as it closes
|
|
37946
|
-
// (see onRequestClose in picker_custom.jsx) — dispatching the action here too
|
|
37947
|
-
// would run it twice. `isSend` tells this close from a dismissal (the cross,
|
|
37948
|
-
// a press outside, Escape): the person pressed a choice, which a picker may
|
|
37949
|
-
// act on even when the choice is the one it already held.
|
|
37950
|
-
const executeNaviDefine = (source, event, target) => {
|
|
37951
|
-
return dispatchCustomEvent(target, "navi_request_close", {
|
|
37952
|
-
event,
|
|
37953
|
-
source: resolveCommandProxySource(source),
|
|
37954
|
-
isSend: true,
|
|
37955
|
-
});
|
|
37956
|
-
};
|
|
37957
|
-
|
|
37958
|
-
registerNaviCommand("--navi-scroll", (source, event) => {
|
|
37959
|
-
const target =
|
|
37960
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
37961
|
-
if (!target) {
|
|
37962
|
-
return undefined;
|
|
37963
|
-
}
|
|
37964
|
-
return {
|
|
37965
|
-
target,
|
|
37966
|
-
implementation: () => {
|
|
37967
|
-
return dispatchCustomEvent(target, "navi_request_scroll", {
|
|
37968
|
-
event,
|
|
37969
|
-
id: resolveCommandValue(source, event),
|
|
37970
|
-
});
|
|
37971
|
-
},
|
|
37972
|
-
};
|
|
37973
|
-
});
|
|
37974
|
-
// The press a hand is still making, given to something that can be carried: a
|
|
37975
|
-
// hold opened a popup, and what the popup holds is to follow that same finger
|
|
37976
|
-
// without it lifting and pressing again. `event` is what the press set off (the
|
|
37977
|
-
// popup's open event), and the press is read from its chain. Run once the popup
|
|
37978
|
-
// has settled, which the caller cannot see: the carry measures its element and
|
|
37979
|
-
// puts a copy in the top layer, neither of which holds while a transition is
|
|
37980
|
-
// still moving the box (see afterPopupLift). What the carry then is — its
|
|
37981
|
-
// outcomes, its refusal — is the element's (see interaction_drag.js).
|
|
37982
|
-
registerNaviCommand("--navi-grab", (source, event) => {
|
|
37983
|
-
const target =
|
|
37984
|
-
resolveExplicitTarget(source) || source.closest("[data-drag-source]");
|
|
37985
|
-
if (!target) {
|
|
37986
|
-
return undefined;
|
|
37987
|
-
}
|
|
37988
|
-
return {
|
|
37989
|
-
target,
|
|
37990
|
-
implementation: () => {
|
|
37991
|
-
afterPopupLift(() => {
|
|
37992
|
-
// Taken away with the popup before it settled.
|
|
37993
|
-
if (!target.isConnected) {
|
|
37994
|
-
return;
|
|
37995
|
-
}
|
|
37996
|
-
dispatchCustomEvent(target, "navi_request_grab", { event });
|
|
37997
|
-
});
|
|
37998
|
-
return true;
|
|
37999
|
-
},
|
|
38000
|
-
};
|
|
38001
|
-
});
|
|
38002
|
-
registerNaviCommand("--navi-check", (source, event) => {
|
|
38003
|
-
const target =
|
|
38004
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38005
|
-
if (!target) {
|
|
38006
|
-
return undefined;
|
|
38007
|
-
}
|
|
38008
|
-
return {
|
|
38009
|
-
target,
|
|
38010
|
-
implementation: () => {
|
|
38011
|
-
return dispatchCustomEvent(target, "navi_request_check", {
|
|
38012
|
-
event,
|
|
38013
|
-
});
|
|
38014
|
-
},
|
|
38015
|
-
};
|
|
38016
|
-
});
|
|
38017
|
-
registerNaviCommand("--navi-uncheck", (source, event) => {
|
|
38018
|
-
const target =
|
|
38019
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38020
|
-
if (!target) {
|
|
38021
|
-
return undefined;
|
|
38022
|
-
}
|
|
38023
|
-
return {
|
|
38024
|
-
target,
|
|
38025
|
-
implementation: () => {
|
|
38026
|
-
return dispatchCustomEvent(target, "navi_request_uncheck", {
|
|
38027
|
-
event,
|
|
38028
|
-
});
|
|
38029
|
-
},
|
|
38030
|
-
};
|
|
38031
|
-
});
|
|
38032
|
-
registerNaviCommand("--navi-select", (source, event) => {
|
|
38033
|
-
const target =
|
|
38034
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38035
|
-
if (!target) {
|
|
38036
|
-
return undefined;
|
|
38037
|
-
}
|
|
38038
|
-
return {
|
|
38039
|
-
target,
|
|
38040
|
-
implementation: () => {
|
|
38041
|
-
return dispatchCustomEvent(target, "navi_request_select", {
|
|
38042
|
-
event,
|
|
38043
|
-
id: resolveCommandValue(source, event),
|
|
38044
|
-
});
|
|
38045
|
-
},
|
|
38046
|
-
};
|
|
38047
|
-
});
|
|
38048
|
-
registerNaviCommand("--navi-unselect", (source, event) => {
|
|
38049
|
-
const target =
|
|
38050
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38051
|
-
if (!target) {
|
|
38052
|
-
return undefined;
|
|
38053
|
-
}
|
|
38054
|
-
return {
|
|
38055
|
-
target,
|
|
38056
|
-
implementation: () => {
|
|
38057
|
-
return dispatchCustomEvent(target, "navi_request_unselect", {
|
|
38058
|
-
event,
|
|
38059
|
-
id: resolveCommandValue(source, event),
|
|
38060
|
-
});
|
|
38061
|
-
},
|
|
38062
|
-
};
|
|
38063
|
-
});
|
|
38064
|
-
|
|
38065
|
-
/**
|
|
38066
|
-
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
38067
|
-
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
38068
|
-
* mounted run inside the call too, exactly as they would on any other commit.
|
|
38069
|
-
*
|
|
38070
|
-
* For the caller that has to read the DOM it just asked for — measuring an
|
|
38071
|
-
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
38072
|
-
* to do it, because what comes after is a browser event still in flight
|
|
38073
|
-
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
38074
|
-
*
|
|
38075
|
-
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
38076
|
-
* render queue drains; swapping it for "right now" for the duration of the call
|
|
38077
|
-
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
38078
|
-
* case above: rendering synchronously in the middle of an event gives up the
|
|
38079
|
-
* batching that makes several state changes one commit.
|
|
38080
|
-
*/
|
|
38081
|
-
const flushSyncRendering = (fn) => {
|
|
38082
|
-
const debounceRenderingPrevious = options.debounceRendering;
|
|
38083
|
-
options.debounceRendering = (drainRenderQueue) => {
|
|
38084
|
-
drainRenderQueue();
|
|
38085
|
-
};
|
|
38086
|
-
try {
|
|
38087
|
-
fn();
|
|
38088
|
-
} finally {
|
|
38089
|
-
options.debounceRendering = debounceRenderingPrevious;
|
|
38090
|
-
}
|
|
38091
|
-
};
|
|
38092
|
-
|
|
38093
|
-
/**
|
|
38094
|
-
* Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
|
|
38095
|
-
* (non-top-layer) renderers — operate on a plain DOM element, no knowledge
|
|
38096
|
-
* of which of the two owns it.
|
|
38097
|
-
*/
|
|
38098
|
-
|
|
38099
|
-
|
|
38100
|
-
/**
|
|
38101
|
-
* Whether a visibleRectEffect delivery is one that can have taken height away
|
|
38102
|
-
* from a popup, and so pushed whatever holds focus out of sight:
|
|
38103
|
-
* - "resize": the window/visual viewport settled — which is also how the
|
|
38104
|
-
* on-screen keyboard arrives, overlay or not (window_size.js);
|
|
38105
|
-
* - ELEMENT_SIZE_CHANGE: the popup's own box measured different;
|
|
38106
|
-
* - "focusin": the focus-settled re-measure, for the room that changes with
|
|
38107
|
-
* nothing announcing it (subscribeFocusSettled in window_size.js).
|
|
38108
|
-
*
|
|
38109
|
-
* Everything else is a scroll of one kind or another, where nothing got
|
|
38110
|
-
* smaller and scrolling the focused element back would fight the very gesture
|
|
38111
|
-
* that fired it.
|
|
38112
|
-
*/
|
|
38113
|
-
const mayHaveHiddenFocus = (event) => {
|
|
38114
|
-
const type = event?.type;
|
|
38115
|
-
return (
|
|
38116
|
-
type === "resize" || type === ELEMENT_SIZE_CHANGE || type === "focusin"
|
|
38117
|
-
);
|
|
38118
|
-
};
|
|
38119
|
-
|
|
38120
|
-
const scrollportHeightMap = new WeakMap();
|
|
38121
|
-
/**
|
|
38122
|
-
* Scrolls whatever holds focus inside `popupEl` back into view, if the popup
|
|
38123
|
-
* getting shorter has pushed it out.
|
|
38124
|
-
*
|
|
38125
|
-
* The case this exists for: a field low in the scrolling body of a popup that
|
|
38126
|
-
* also has a footer (box.jsx — with a body, the body is the only thing that
|
|
38127
|
-
* scrolls and the footer is a sibling sitting right under it). Focusing the
|
|
38128
|
-
* field makes the browser scroll it into view, which it does against the
|
|
38129
|
-
* popup's height AT THAT MOMENT; the on-screen keyboard then opens and takes
|
|
38130
|
-
* that height away. The body shrinks, its scrollTop does not move, so the
|
|
38131
|
-
* content slides down relative to the shorter scrollport and the field ends up
|
|
38132
|
-
* past its bottom edge — visually, swallowed by the footer. The browser does
|
|
38133
|
-
* not redo a scroll-into-view it already answered, so this does.
|
|
38134
|
-
*
|
|
38135
|
-
* Only what the shrink itself hid, though: with the keyboard up the user reads
|
|
38136
|
-
* the rest of the popup by scrolling the field away — to reach the submit under
|
|
38137
|
-
* it, typically — and the room keeps changing while they do (a keyboard settling
|
|
38138
|
-
* in two steps, a suggestion strip, a browser bar). Answering each of those by
|
|
38139
|
-
* scrolling the field back takes the popup away from wherever they had just
|
|
38140
|
-
* scrolled it, over and over: what they were reading cannot be reached at all
|
|
38141
|
-
* without blurring the field first, and the popup reads as unscrollable. So the
|
|
38142
|
-
* field is brought back only when it was in view before the room shrank, which
|
|
38143
|
-
* one remembered number answers: a resize moves neither scrollTop nor the
|
|
38144
|
-
* element's offset inside the scrolled content, so measuring against the height
|
|
38145
|
-
* the scrollport HAD is measuring the state before the change.
|
|
38146
|
-
*
|
|
38147
|
-
* Scoped to the field's own scroll container (never the page): a popup traps
|
|
38148
|
-
* scrolling precisely so the document underneath cannot move, and a plain
|
|
38149
|
-
* scrollIntoView walks past a container whose scrollbar isn't visible — see
|
|
38150
|
-
* scrollIntoViewScoped's own doc.
|
|
38151
|
-
*
|
|
38152
|
-
* "nearest": the smallest scroll that makes it visible, and none at all when it
|
|
38153
|
-
* already is. Where it lands is the container's own business — a navi scroller
|
|
38154
|
-
* keeps a band free at its edges so a field never comes back glued to one (see
|
|
38155
|
-
* scroll-padding in box.jsx).
|
|
38156
|
-
*/
|
|
38157
|
-
const keepFocusedElementVisible = (popupEl) => {
|
|
38158
|
-
const { activeElement } = document;
|
|
38159
|
-
if (!activeElement || activeElement === popupEl) {
|
|
38160
|
-
return;
|
|
38161
|
-
}
|
|
38162
|
-
if (!popupEl.contains(activeElement)) {
|
|
38163
|
-
return;
|
|
38164
|
-
}
|
|
38165
|
-
const scrollContainer = getScrollContainer(activeElement);
|
|
38166
|
-
if (!scrollContainer || !popupEl.contains(scrollContainer)) {
|
|
38167
|
-
// What scrolls the field is outside the popup, which means the page: a
|
|
38168
|
-
// popup holds it still on purpose (trapScrollInside), so there is nothing
|
|
38169
|
-
// here to scroll back.
|
|
38170
|
-
return;
|
|
38171
|
-
}
|
|
38172
|
-
const scrollportHeight = scrollContainer.clientHeight;
|
|
38173
|
-
const scrollportHeightBefore = scrollportHeightMap.get(scrollContainer);
|
|
38174
|
-
scrollportHeightMap.set(scrollContainer, scrollportHeight);
|
|
38175
|
-
if (scrollportHeightBefore !== undefined) {
|
|
38176
|
-
if (scrollportHeight >= scrollportHeightBefore) {
|
|
38177
|
-
// Nothing was taken away, so nothing was hidden by this.
|
|
38178
|
-
return;
|
|
38179
|
-
}
|
|
38180
|
-
if (
|
|
38181
|
-
!isVisibleInScrollport(
|
|
38182
|
-
activeElement,
|
|
38183
|
-
scrollContainer,
|
|
38184
|
-
scrollportHeightBefore,
|
|
38185
|
-
)
|
|
38186
|
-
) {
|
|
38187
|
-
return;
|
|
38188
|
-
}
|
|
38189
|
-
}
|
|
38190
|
-
scrollIntoViewScoped(activeElement, {
|
|
38191
|
-
container: scrollContainer,
|
|
38192
|
-
block: "nearest",
|
|
38193
|
-
});
|
|
38194
|
-
};
|
|
38195
|
-
|
|
38196
|
-
// Whether any part of `el` was showing in `container` back when its scrollport
|
|
38197
|
-
// was `scrollportHeight` tall. Both boxes are read now: the container may have
|
|
38198
|
-
// moved as well as shrunk (a centered dialog re-centers itself), and the
|
|
38199
|
-
// difference between the two tops is what that move leaves alone.
|
|
38200
|
-
const isVisibleInScrollport = (el, container, scrollportHeight) => {
|
|
38201
|
-
const elRect = el.getBoundingClientRect();
|
|
38202
|
-
const containerRect = container.getBoundingClientRect();
|
|
38203
|
-
const elTop = elRect.top - containerRect.top + container.scrollTop;
|
|
38204
|
-
const elBottom = elTop + elRect.height;
|
|
38205
|
-
const scrollportTop = container.scrollTop;
|
|
38206
|
-
const scrollportBottom = scrollportTop + scrollportHeight;
|
|
38207
|
-
return elBottom > scrollportTop && elTop < scrollportBottom;
|
|
38208
|
-
};
|
|
38209
|
-
|
|
38210
|
-
/**
|
|
38211
|
-
* Calls `onSettled` once `el`'s current CSS transition is over — via
|
|
38212
|
-
* `transitionend`, with a safety `setTimeout` fallback matching the longest
|
|
38213
|
-
* `transition-duration`, in case nothing actually transitions or an event is
|
|
38214
|
-
* missed.
|
|
38215
|
-
*
|
|
38216
|
-
* Returns a "cancel" function, so a caller whose instance has been superseded
|
|
38217
|
-
* (a fresh open/close about to set its own state) can keep this stale one from
|
|
38218
|
-
* firing later. Cancelling only stops `onSettled`: undoing whatever the caller
|
|
38219
|
-
* did up front is that fresh call's business, not this one's.
|
|
38220
|
-
*/
|
|
38221
|
-
const whenTransitionSettles = (el, onSettled) => {
|
|
38222
|
-
let settled = false;
|
|
38223
|
-
const onTransitionEnd = (transitionEvent) => {
|
|
38224
|
-
if (transitionEvent.target === el) {
|
|
38225
|
-
finish();
|
|
38226
|
-
}
|
|
38227
|
-
};
|
|
38228
|
-
const stopWatching = () => {
|
|
38229
|
-
settled = true;
|
|
38230
|
-
el.removeEventListener("transitionend", onTransitionEnd);
|
|
38231
|
-
clearTimeout(safetyTimeoutId);
|
|
38232
|
-
};
|
|
38233
|
-
const finish = () => {
|
|
38234
|
-
if (settled) {
|
|
38235
|
-
return;
|
|
38236
|
-
}
|
|
38237
|
-
stopWatching();
|
|
38238
|
-
onSettled();
|
|
38239
|
-
};
|
|
38240
|
-
el.addEventListener("transitionend", onTransitionEnd);
|
|
38241
|
-
const durationsInSeconds = getComputedStyle(el)
|
|
38242
|
-
.transitionDuration.split(",")
|
|
38243
|
-
.map((value) => parseFloat(value) || 0);
|
|
38244
|
-
const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
|
|
38245
|
-
const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
|
|
38246
|
-
return () => {
|
|
38247
|
-
if (settled) {
|
|
38248
|
-
return;
|
|
38249
|
-
}
|
|
38250
|
-
stopWatching();
|
|
38251
|
-
};
|
|
38252
|
-
};
|
|
38253
|
-
|
|
38254
|
-
/**
|
|
38255
|
-
* Drops the document's text selection when it lives inside `el`, leaving a
|
|
38256
|
-
* selection made elsewhere on the page alone.
|
|
38257
|
-
*
|
|
38258
|
-
* A popup being closed takes its content with it, and a selection is a claim
|
|
38259
|
-
* on content the user can still act on: kept, it would outlive the surface it
|
|
38260
|
-
* was made on — painted on the box for the length of its exit transition, with
|
|
38261
|
-
* the handles and the copy toolbar a phone draws from the live selection
|
|
38262
|
-
* hanging over something that is going away — and reappear with the box on
|
|
38263
|
-
* the next opening. Dropping the selection is the only way to remove that
|
|
38264
|
-
* chrome: a `user-select: none` on the closing box hides the highlight in some
|
|
38265
|
-
* browsers only, and the handles are the browser's own, drawn from the
|
|
38266
|
-
* selection object rather than from any style.
|
|
38267
|
-
*/
|
|
38268
|
-
const clearTextSelectionInside = (el) => {
|
|
38269
|
-
const selection = window.getSelection();
|
|
38270
|
-
if (!selection || selection.rangeCount === 0) {
|
|
38271
|
-
return;
|
|
38272
|
-
}
|
|
38273
|
-
const range = selection.getRangeAt(0);
|
|
38274
|
-
if (!el.contains(range.commonAncestorContainer)) {
|
|
38275
|
-
return;
|
|
38276
|
-
}
|
|
38277
|
-
selection.removeAllRanges();
|
|
38278
|
-
};
|
|
38279
|
-
|
|
38280
|
-
/**
|
|
38281
|
-
* Disables pointer-events on `el` until its current CSS transition settles —
|
|
38282
|
-
* avoids the cursor changing/something becoming clickable while the popup is
|
|
38283
|
-
* still visually moving into or out of place.
|
|
38284
|
-
*
|
|
38285
|
-
* Returns whenTransitionSettles' own "cancel" function: it doesn't restore
|
|
38286
|
-
* pointer-events, since a fresh call for the next open/close is about to set
|
|
38287
|
-
* its own state.
|
|
38288
|
-
*/
|
|
38289
|
-
const suppressPointerEventsDuringTransition = (el) => {
|
|
38290
|
-
el.style.pointerEvents = "none";
|
|
38291
|
-
return whenTransitionSettles(el, () => {
|
|
38292
|
-
el.style.pointerEvents = "";
|
|
38293
|
-
});
|
|
38294
|
-
};
|
|
38295
|
-
|
|
38296
|
-
/**
|
|
38297
|
-
* Hides the backdrop, deferring until the browser's matching "click" fires
|
|
38298
|
-
* when `closeEvent` was triggered by a mousedown (see popover.jsx's top
|
|
38299
|
-
* comment for why) — same capture-phase-on-document pattern as
|
|
38300
|
-
* armSuppressNextOpenRequest in open_controller.js, which a plain timeout
|
|
38301
|
-
* can't safely replace: mouseup (and the click that follows it) can land an
|
|
38302
|
-
* arbitrarily long time after mousedown (the user is still holding the
|
|
38303
|
-
* button down), so a short timeout can fire first and hide the backdrop
|
|
38304
|
-
* before its own click ever arrives. A capture-phase listener on document
|
|
38305
|
-
* fires for every click regardless of what any bubble-phase handler does
|
|
38306
|
-
* downstream, so no fallback timer is needed.
|
|
38307
|
-
*
|
|
38308
|
-
* `hide` is the caller's own way to actually hide the backdrop
|
|
38309
|
-
* (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
|
|
38310
|
-
* for a plain div) — this helper only owns the mousedown/click timing.
|
|
38311
|
-
*
|
|
38312
|
-
* Returns a disarm function (or undefined if hidden immediately), so a
|
|
38313
|
-
* fresh open can cancel a pending hide it's about to make redundant.
|
|
38314
|
-
*/
|
|
38315
|
-
const armPointerDownOutsideClose = (closeEvent, hide) => {
|
|
38316
|
-
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
38317
|
-
if (!mousedownEvent) {
|
|
38318
|
-
hide();
|
|
38319
|
-
return undefined;
|
|
38320
|
-
}
|
|
38321
|
-
const onClick = () => {
|
|
38322
|
-
document.removeEventListener("click", onClick, { capture: true });
|
|
38323
|
-
hide();
|
|
38324
|
-
};
|
|
38325
|
-
document.addEventListener("click", onClick, { capture: true });
|
|
38326
|
-
return () => {
|
|
38327
|
-
document.removeEventListener("click", onClick, { capture: true });
|
|
38328
|
-
};
|
|
38329
|
-
};
|
|
38330
|
-
|
|
38331
|
-
/**
|
|
38332
|
-
* Maps a positionArea y/x pair to a concrete `navi-animation` value (a
|
|
38333
|
-
* `prefix` plus a direction word), or `null` if both axes overlap the anchor
|
|
38334
|
-
* (no direction at all — that's `resolvedAnimationKind === "scaling"`
|
|
38335
|
-
* territory instead, see resolveAutoAnimationKind below).
|
|
38336
|
-
*
|
|
38337
|
-
* `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
|
|
38338
|
-
* when docked) keeps the word as the compass direction the popup comes
|
|
38339
|
-
* from: placed "top" (a point/corner), it slides in from the top.
|
|
38340
|
-
* `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
|
|
38341
|
-
* direction instead, the opposite compass point: placed "top" of the
|
|
38342
|
-
* anchor, it moves/grows up, away from the anchor (which sits below it).
|
|
38343
|
-
*
|
|
38344
|
-
* "inset-*"/"center" contribute no direction on their axis either way.
|
|
38345
|
-
*/
|
|
38346
|
-
const resolveDirectionValue = (y, x, { prefix }) => {
|
|
38347
|
-
const yWord =
|
|
38348
|
-
y === "top"
|
|
38349
|
-
? prefix === "expand"
|
|
38350
|
-
? "up"
|
|
38351
|
-
: "top"
|
|
38352
|
-
: y === "bottom"
|
|
38353
|
-
? prefix === "expand"
|
|
38354
|
-
? "down"
|
|
38355
|
-
: "bottom"
|
|
38356
|
-
: null;
|
|
38357
|
-
const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
|
|
38358
|
-
if (!yWord && !xWord) {
|
|
38359
|
-
return null;
|
|
38360
|
-
}
|
|
38361
|
-
return yWord && xWord
|
|
38362
|
-
? `${prefix}-${yWord}-${xWord}`
|
|
38363
|
-
: `${prefix}-${yWord || xWord}`;
|
|
38364
|
-
};
|
|
38365
|
-
|
|
38366
|
-
/**
|
|
38367
|
-
* Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
|
|
38368
|
-
* — picked for any real anchor, or for a point/corner placed dead-center
|
|
38369
|
-
* (both positionArea axes overlapping — there's no sensible direction to
|
|
38370
|
-
* slide from in that case). "sliding" otherwise. `anchor` is `undefined`
|
|
38371
|
-
* for any no-anchor/docked case (Dialog always, Popover's own custom
|
|
38372
|
-
* renderer when there's no real anchor), so this collapses to "scaling"
|
|
38373
|
-
* there only for the dead-center case, "sliding" otherwise. The two
|
|
38374
|
-
* "overlapping" booleans below describe the *positionArea* itself (a bare
|
|
38375
|
-
* word vs. "inset-"/"center"), not anything about the anchor — they'd
|
|
38376
|
-
* mean exactly the same thing even with no anchor at all, since it's the
|
|
38377
|
-
* position strategy, not the anchor, that decides whether there's a
|
|
38378
|
-
* direction to slide from.
|
|
38379
|
-
*/
|
|
38380
|
-
const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
|
|
38381
|
-
const yIsOverlapping =
|
|
38382
|
-
parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
|
|
38383
|
-
const xIsOverlapping =
|
|
38384
|
-
parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
|
|
38385
|
-
return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
|
|
38386
|
-
};
|
|
38387
|
-
|
|
38388
|
-
/**
|
|
38389
|
-
* A popup reads "outside" from its own border box: what a press lands on
|
|
38390
|
-
* decides nothing on its own (a genuine backdrop press and a press on the
|
|
38391
|
-
* popup's padding both report the popup element as their target, there being
|
|
38392
|
-
* no real ::backdrop node to be one), so the rectangle is what tells them
|
|
38393
|
-
* apart. That holds as long as the box and what the popup paints are the same
|
|
38394
|
-
* thing — which stops being true for a popup with no surface of its own
|
|
38395
|
-
* (`backgroundColor="transparent"`, no shadow, no padding): what the eye reads
|
|
38396
|
-
* as backdrop is then inside the box, and a press there stays a press on the
|
|
38397
|
-
* popup.
|
|
38398
|
-
*
|
|
38399
|
-
* `data-navi-popup-outside` is how a caller says which of its own boxes are
|
|
38400
|
-
* not the surface. It is opt-in because navi cannot infer it — a background
|
|
38401
|
-
* can come from anywhere — while the caller who made the popup see-through
|
|
38402
|
-
* knows exactly which box is decoration and which is paper.
|
|
38403
|
-
*
|
|
38404
|
-
* The marker answers for the element it is on, never for its descendants: a
|
|
38405
|
-
* box painted inside a marked one is still surface, so a row can be marked and
|
|
38406
|
-
* its empty halves read as backdrop while presses on the controls it holds do
|
|
38407
|
-
* not dismiss anything. A descendant with `pointer-events: none` never becomes
|
|
38408
|
-
* a press target at all, so the marked box answers in its place — while the
|
|
38409
|
-
* same declaration on the marked box itself takes it out of hit-testing and
|
|
38410
|
-
* makes the marker unreachable (see warnAboutUnreachableOutsideRegions).
|
|
38411
|
-
*/
|
|
38412
|
-
const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
|
|
38413
|
-
|
|
38414
|
-
/**
|
|
38415
|
-
* `data-navi-popup-inside` is the other direction: a box of the PAGE the
|
|
38416
|
-
* caller says belongs to a popup, so a press on it is not outside that popup
|
|
38417
|
-
* even though it lies beyond the border box — a card whose press is what
|
|
38418
|
-
* fills the one panel of a board. Its value names the popup by `id`, the way
|
|
38419
|
-
* `commandfor` does (several ids, space separated), because the exemption is
|
|
38420
|
-
* one popup's: a menu open beside the board still closes on the same press.
|
|
38421
|
-
*
|
|
38422
|
-
* Unlike the outside marker it answers for its whole subtree: the outside
|
|
38423
|
-
* marker carves free space out of a surface whose controls stay surface, this
|
|
38424
|
-
* one adds a whole thing — the card, its count, its buttons — to the popup's
|
|
38425
|
-
* own ground, and nothing in it is a dismissal.
|
|
38426
|
-
*/
|
|
38427
|
-
const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
|
|
38428
|
-
|
|
38605
|
+
// Back to the screen the reader came from, with somewhere to land when there
|
|
38606
|
+
// is none of ours behind — a url opened cold (see navBack). The fallback is
|
|
38607
|
+
// the argument for the reason --navi-nav-to's destination is: it says WHAT the
|
|
38608
|
+
// command does when there is nothing to go back to.
|
|
38609
|
+
registerNaviCommand("--navi-nav-back", (source, event, { argument }) => {
|
|
38610
|
+
const target = resolveExplicitTarget(source) || source;
|
|
38611
|
+
return {
|
|
38612
|
+
target,
|
|
38613
|
+
implementation: () => navBack({ fallback: argument }),
|
|
38614
|
+
};
|
|
38615
|
+
});
|
|
38616
|
+
|
|
38617
|
+
registerNaviCommand(
|
|
38618
|
+
"--navi-toggle",
|
|
38619
|
+
(source, event, { anchor, value } = {}) => {
|
|
38620
|
+
const target =
|
|
38621
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38622
|
+
if (!target) {
|
|
38623
|
+
return undefined;
|
|
38624
|
+
}
|
|
38625
|
+
return {
|
|
38626
|
+
target,
|
|
38627
|
+
implementation: () => {
|
|
38628
|
+
const isExpanded = target.getAttribute("aria-expanded") === "true";
|
|
38629
|
+
const customEventName = isExpanded
|
|
38630
|
+
? "navi_request_close"
|
|
38631
|
+
: "navi_request_open";
|
|
38632
|
+
return dispatchCustomEvent(target, customEventName, {
|
|
38633
|
+
event,
|
|
38634
|
+
source: resolveCommandProxySource(source),
|
|
38635
|
+
anchor,
|
|
38636
|
+
// Same as --navi-open below: the half of the toggle that opens says
|
|
38637
|
+
// what it opens ON, and the half that closes carries it too so the
|
|
38638
|
+
// popup never has to ask which half it just heard.
|
|
38639
|
+
value:
|
|
38640
|
+
value === undefined ? resolveCommandValue(source, event) : value,
|
|
38641
|
+
});
|
|
38642
|
+
},
|
|
38643
|
+
};
|
|
38644
|
+
},
|
|
38645
|
+
);
|
|
38646
|
+
// A popup that edits opens ON something, and the press is the only place that
|
|
38647
|
+
// knows which one. It says it with `value` — "what this is about", as
|
|
38648
|
+
// everywhere else — rather than with an argument, which says what the command
|
|
38649
|
+
// DOES: "open" is already a complete instruction, unlike --navi-go-to-slide.
|
|
38650
|
+
// <Button value={radar.id} command="--navi-open" commandfor="radar-dialog">
|
|
38651
|
+
// The popup is told before it opens — see Dialog/Popover's `onOpen`.
|
|
38652
|
+
registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
|
|
38653
|
+
const target =
|
|
38654
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38655
|
+
if (!target) {
|
|
38656
|
+
return undefined;
|
|
38657
|
+
}
|
|
38658
|
+
return {
|
|
38659
|
+
target,
|
|
38660
|
+
implementation: () => {
|
|
38661
|
+
// The popup reads `anchor` first and falls back to the source (see
|
|
38662
|
+
// onnavi_request_open in popover.jsx): without an `anchor`, the element
|
|
38663
|
+
// asking is the element opened against.
|
|
38664
|
+
return dispatchCustomEvent(target, "navi_request_open", {
|
|
38665
|
+
event,
|
|
38666
|
+
source: resolveCommandProxySource(source),
|
|
38667
|
+
anchor,
|
|
38668
|
+
value: value === undefined ? resolveCommandValue(source, event) : value,
|
|
38669
|
+
});
|
|
38670
|
+
},
|
|
38671
|
+
};
|
|
38672
|
+
});
|
|
38429
38673
|
/**
|
|
38430
|
-
*
|
|
38431
|
-
*
|
|
38674
|
+
* Tells the popup a source opens that a press on it has started, ahead of the
|
|
38675
|
+
* release that runs the command: what the opening will read can be asked for
|
|
38676
|
+
* now (see open_controller.js). A press rather than a hover, because what
|
|
38677
|
+
* that reads is keyed by the popup — one sheet per card — and a pointer
|
|
38678
|
+
* crossing a list of cards has chosen none of them; on a touch screen the
|
|
38679
|
+
* press is the first thing the finger says anyway.
|
|
38432
38680
|
*
|
|
38433
|
-
*
|
|
38434
|
-
*
|
|
38435
|
-
* (never an ancestor), so a press inside the popup's box never reaches it.
|
|
38681
|
+
* The target is found the way --navi-open finds it, but silently: nothing
|
|
38682
|
+
* runs here, and a source with no target says so at the release.
|
|
38436
38683
|
*/
|
|
38437
|
-
const
|
|
38438
|
-
|
|
38439
|
-
{ popupEl, openController, pressOutside },
|
|
38440
|
-
) => {
|
|
38441
|
-
if (mouseDownEvent.button !== 0) {
|
|
38684
|
+
const announceOpeningPress = (source, command) => {
|
|
38685
|
+
if (command !== "--navi-open" && command !== "--navi-toggle") {
|
|
38442
38686
|
return;
|
|
38443
38687
|
}
|
|
38444
|
-
const
|
|
38445
|
-
|
|
38688
|
+
const commandFor = source.getAttribute("commandfor");
|
|
38689
|
+
const target = commandFor
|
|
38690
|
+
? document.getElementById(commandFor)
|
|
38691
|
+
: resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38692
|
+
if (!target) {
|
|
38446
38693
|
return;
|
|
38447
38694
|
}
|
|
38448
|
-
|
|
38449
|
-
|
|
38450
|
-
|
|
38451
|
-
|
|
38695
|
+
dispatchCustomEvent(target, "navi_open_press");
|
|
38696
|
+
};
|
|
38697
|
+
// "--navi-close:all" closes every expandable above the source, nearest first —
|
|
38698
|
+
// a link leaving from a badge shown over a sheet leaves both. A surface that
|
|
38699
|
+
// refuses (a form asking about its changes) keeps what is above it open too:
|
|
38700
|
+
// one cannot be out of the sheet while still in the badge.
|
|
38701
|
+
registerNaviCommand("--navi-close", (source, event, { argument }) => {
|
|
38702
|
+
const target =
|
|
38703
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38704
|
+
if (!target) {
|
|
38705
|
+
return undefined;
|
|
38452
38706
|
}
|
|
38453
|
-
|
|
38454
|
-
|
|
38455
|
-
|
|
38456
|
-
|
|
38457
|
-
|
|
38458
|
-
|
|
38459
|
-
|
|
38460
|
-
|
|
38707
|
+
return {
|
|
38708
|
+
target,
|
|
38709
|
+
implementation: () => {
|
|
38710
|
+
const detail = { event, source: resolveCommandProxySource(source) };
|
|
38711
|
+
if (argument === "all") {
|
|
38712
|
+
return requestCloseUpward(target, detail);
|
|
38713
|
+
}
|
|
38714
|
+
return dispatchCustomEvent(target, "navi_request_close", detail);
|
|
38715
|
+
},
|
|
38716
|
+
};
|
|
38717
|
+
});
|
|
38718
|
+
const requestCloseUpward = (target, detail) => {
|
|
38719
|
+
let expandable = target;
|
|
38720
|
+
while (expandable) {
|
|
38721
|
+
const closing = dispatchCustomEvent(
|
|
38722
|
+
expandable,
|
|
38723
|
+
"navi_request_close",
|
|
38724
|
+
detail,
|
|
38725
|
+
);
|
|
38726
|
+
if (!closing) {
|
|
38727
|
+
return false;
|
|
38728
|
+
}
|
|
38729
|
+
const parent = expandable.parentElement;
|
|
38730
|
+
expandable = parent ? parent.closest("[aria-expanded]") : null;
|
|
38461
38731
|
}
|
|
38462
|
-
|
|
38463
|
-
|
|
38464
|
-
|
|
38732
|
+
return true;
|
|
38733
|
+
};
|
|
38734
|
+
registerNaviCommand("--navi-cancel", (source, event) => {
|
|
38735
|
+
const target =
|
|
38736
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38737
|
+
if (!target) {
|
|
38738
|
+
return undefined;
|
|
38465
38739
|
}
|
|
38466
|
-
|
|
38467
|
-
|
|
38468
|
-
|
|
38469
|
-
|
|
38740
|
+
return {
|
|
38741
|
+
target,
|
|
38742
|
+
implementation: () => {
|
|
38743
|
+
return dispatchCustomEvent(target, "navi_request_close", {
|
|
38744
|
+
event,
|
|
38745
|
+
source: resolveCommandProxySource(source),
|
|
38746
|
+
isCancel: true,
|
|
38747
|
+
});
|
|
38748
|
+
},
|
|
38749
|
+
};
|
|
38750
|
+
});
|
|
38751
|
+
// "Yes, do it" — said inside the popup of a confirm picker (see
|
|
38752
|
+
// picker/picker_confirm.jsx), by its own yes button or by whatever a caller put
|
|
38753
|
+
// there instead. Its counterpart is --navi-cancel; the difference between the
|
|
38754
|
+
// two is this event, since both then close the popup, and a popup that closes
|
|
38755
|
+
// without having said this is the answer "no".
|
|
38756
|
+
registerNaviCommand("--navi-confirm", (source, event) => {
|
|
38757
|
+
const target =
|
|
38758
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38759
|
+
if (!target) {
|
|
38760
|
+
return undefined;
|
|
38761
|
+
}
|
|
38762
|
+
return {
|
|
38763
|
+
target,
|
|
38764
|
+
implementation: () => {
|
|
38765
|
+
const commandSource = resolveCommandProxySource(source);
|
|
38766
|
+
dispatchCustomEvent(target, "navi_request_confirm", {
|
|
38767
|
+
event,
|
|
38768
|
+
source: commandSource,
|
|
38769
|
+
});
|
|
38770
|
+
return dispatchCustomEvent(target, "navi_request_close", {
|
|
38771
|
+
event,
|
|
38772
|
+
source: commandSource,
|
|
38773
|
+
});
|
|
38774
|
+
},
|
|
38775
|
+
};
|
|
38776
|
+
});
|
|
38777
|
+
registerNaviCommand("--navi-define", (source, event) => {
|
|
38778
|
+
const target =
|
|
38779
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38780
|
+
if (!target) {
|
|
38781
|
+
return undefined;
|
|
38470
38782
|
}
|
|
38783
|
+
return {
|
|
38784
|
+
target,
|
|
38785
|
+
implementation: () => executeNaviDefine(source, event, target),
|
|
38786
|
+
};
|
|
38787
|
+
});
|
|
38788
|
+
// A send whose target is the popup itself: what is around the source IS the
|
|
38789
|
+
// popup's answer, and a picker runs its action with that answer as it closes
|
|
38790
|
+
// (see onRequestClose in picker_custom.jsx) — dispatching the action here too
|
|
38791
|
+
// would run it twice. `isSend` tells this close from a dismissal (the cross,
|
|
38792
|
+
// a press outside, Escape): the person pressed a choice, which a picker may
|
|
38793
|
+
// act on even when the choice is the one it already held.
|
|
38794
|
+
const executeNaviDefine = (source, event, target) => {
|
|
38795
|
+
return dispatchCustomEvent(target, "navi_request_close", {
|
|
38796
|
+
event,
|
|
38797
|
+
source: resolveCommandProxySource(source),
|
|
38798
|
+
isSend: true,
|
|
38799
|
+
});
|
|
38471
38800
|
};
|
|
38472
38801
|
|
|
38802
|
+
registerNaviCommand("--navi-scroll", (source, event) => {
|
|
38803
|
+
const target =
|
|
38804
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38805
|
+
if (!target) {
|
|
38806
|
+
return undefined;
|
|
38807
|
+
}
|
|
38808
|
+
return {
|
|
38809
|
+
target,
|
|
38810
|
+
implementation: () => {
|
|
38811
|
+
return dispatchCustomEvent(target, "navi_request_scroll", {
|
|
38812
|
+
event,
|
|
38813
|
+
id: resolveCommandValue(source, event),
|
|
38814
|
+
});
|
|
38815
|
+
},
|
|
38816
|
+
};
|
|
38817
|
+
});
|
|
38818
|
+
// The press a hand is still making, given to something that can be carried: a
|
|
38819
|
+
// hold opened a popup, and what the popup holds is to follow that same finger
|
|
38820
|
+
// without it lifting and pressing again. `event` is what the press set off (the
|
|
38821
|
+
// popup's open event), and the press is read from its chain. Run once the popup
|
|
38822
|
+
// has settled, which the caller cannot see: the carry measures its element and
|
|
38823
|
+
// puts a copy in the top layer, neither of which holds while a transition is
|
|
38824
|
+
// still moving the box (see afterPopupLift). What the carry then is — its
|
|
38825
|
+
// outcomes, its refusal — is the element's (see interaction_drag.js).
|
|
38826
|
+
registerNaviCommand("--navi-grab", (source, event) => {
|
|
38827
|
+
const target =
|
|
38828
|
+
resolveExplicitTarget(source) || source.closest("[data-drag-source]");
|
|
38829
|
+
if (!target) {
|
|
38830
|
+
return undefined;
|
|
38831
|
+
}
|
|
38832
|
+
return {
|
|
38833
|
+
target,
|
|
38834
|
+
implementation: () => {
|
|
38835
|
+
afterPopupLift(() => {
|
|
38836
|
+
// Taken away with the popup before it settled.
|
|
38837
|
+
if (!target.isConnected) {
|
|
38838
|
+
return;
|
|
38839
|
+
}
|
|
38840
|
+
dispatchCustomEvent(target, "navi_request_grab", { event });
|
|
38841
|
+
});
|
|
38842
|
+
return true;
|
|
38843
|
+
},
|
|
38844
|
+
};
|
|
38845
|
+
});
|
|
38846
|
+
registerNaviCommand("--navi-check", (source, event) => {
|
|
38847
|
+
const target =
|
|
38848
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38849
|
+
if (!target) {
|
|
38850
|
+
return undefined;
|
|
38851
|
+
}
|
|
38852
|
+
return {
|
|
38853
|
+
target,
|
|
38854
|
+
implementation: () => {
|
|
38855
|
+
return dispatchCustomEvent(target, "navi_request_check", {
|
|
38856
|
+
event,
|
|
38857
|
+
});
|
|
38858
|
+
},
|
|
38859
|
+
};
|
|
38860
|
+
});
|
|
38861
|
+
registerNaviCommand("--navi-uncheck", (source, event) => {
|
|
38862
|
+
const target =
|
|
38863
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38864
|
+
if (!target) {
|
|
38865
|
+
return undefined;
|
|
38866
|
+
}
|
|
38867
|
+
return {
|
|
38868
|
+
target,
|
|
38869
|
+
implementation: () => {
|
|
38870
|
+
return dispatchCustomEvent(target, "navi_request_uncheck", {
|
|
38871
|
+
event,
|
|
38872
|
+
});
|
|
38873
|
+
},
|
|
38874
|
+
};
|
|
38875
|
+
});
|
|
38876
|
+
registerNaviCommand("--navi-select", (source, event) => {
|
|
38877
|
+
const target =
|
|
38878
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38879
|
+
if (!target) {
|
|
38880
|
+
return undefined;
|
|
38881
|
+
}
|
|
38882
|
+
return {
|
|
38883
|
+
target,
|
|
38884
|
+
implementation: () => {
|
|
38885
|
+
return dispatchCustomEvent(target, "navi_request_select", {
|
|
38886
|
+
event,
|
|
38887
|
+
id: resolveCommandValue(source, event),
|
|
38888
|
+
});
|
|
38889
|
+
},
|
|
38890
|
+
};
|
|
38891
|
+
});
|
|
38892
|
+
registerNaviCommand("--navi-unselect", (source, event) => {
|
|
38893
|
+
const target =
|
|
38894
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38895
|
+
if (!target) {
|
|
38896
|
+
return undefined;
|
|
38897
|
+
}
|
|
38898
|
+
return {
|
|
38899
|
+
target,
|
|
38900
|
+
implementation: () => {
|
|
38901
|
+
return dispatchCustomEvent(target, "navi_request_unselect", {
|
|
38902
|
+
event,
|
|
38903
|
+
id: resolveCommandValue(source, event),
|
|
38904
|
+
});
|
|
38905
|
+
},
|
|
38906
|
+
};
|
|
38907
|
+
});
|
|
38908
|
+
|
|
38473
38909
|
/**
|
|
38474
|
-
*
|
|
38475
|
-
*
|
|
38476
|
-
*
|
|
38477
|
-
* caller asked to leave the page reachable (`backdrop={false}`).
|
|
38478
|
-
*
|
|
38479
|
-
* The press is read and never taken: no `preventDefault`, no
|
|
38480
|
-
* `stopPropagation`, and the listener is on `document` rather than on
|
|
38481
|
-
* anything covering the page — so what the press landed on hears it too, in
|
|
38482
|
-
* the same gesture, and the popup only takes note. That is the whole point of
|
|
38483
|
-
* a popup with no wall: a press outside is a press on the page, and the page
|
|
38484
|
-
* answers it. The one popup where that cannot hold is the modal dialog, where
|
|
38485
|
-
* the browser has already made everything behind inert before this runs.
|
|
38486
|
-
*
|
|
38487
|
-
* `pressEventType` says which event IS the press, and the two callers want
|
|
38488
|
-
* opposite ones:
|
|
38489
|
-
*
|
|
38490
|
-
* - `"pointerdown"` for a popup with no wall: a page with no wall over it is
|
|
38491
|
-
* a page whose own elements arbitrate their presses, and cancelling a
|
|
38492
|
-
* `pointerdown` — what a drag source and a control keeping the focus where
|
|
38493
|
-
* it is both do — suppresses every compatibility mouse event that would have
|
|
38494
|
-
* followed it. The press happens, reaches its target, and no `mousedown` is
|
|
38495
|
-
* ever dispatched for it. Reading the pointer event is reading the press
|
|
38496
|
-
* itself, whatever anything does with it afterwards, and it is the same
|
|
38497
|
-
* event the drag and surface detectors read.
|
|
38498
|
-
* - `"mousedown"` for the modal dialog, whose native `::backdrop` exists to
|
|
38499
|
-
* spend the press. The page's controls act on `mousedown`, and on a touch
|
|
38500
|
-
* screen that is not the same moment as `pointerdown`: the compatibility
|
|
38501
|
-
* mouse events are synthesized at `touchend` and hit-tested again then.
|
|
38502
|
-
* Closing on `pointerdown` takes the `::backdrop` away before that second
|
|
38503
|
-
* hit-test, so the `mousedown` lands on whatever the page holds under the
|
|
38504
|
-
* finger, and one tap both dismisses the dialog and presses the page. Read
|
|
38505
|
-
* on `mousedown`, the wall is still up when the press the page acts on is
|
|
38506
|
-
* aimed, and it catches it. Nothing behind a modal wall can cancel that
|
|
38507
|
-
* press, since nothing behind it hears the `pointerdown` at all.
|
|
38910
|
+
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
38911
|
+
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
38912
|
+
* mounted run inside the call too, exactly as they would on any other commit.
|
|
38508
38913
|
*
|
|
38509
|
-
*
|
|
38510
|
-
*
|
|
38914
|
+
* For the caller that has to read the DOM it just asked for — measuring an
|
|
38915
|
+
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
38916
|
+
* to do it, because what comes after is a browser event still in flight
|
|
38917
|
+
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
38511
38918
|
*
|
|
38512
|
-
*
|
|
38919
|
+
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
38920
|
+
* render queue drains; swapping it for "right now" for the duration of the call
|
|
38921
|
+
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
38922
|
+
* case above: rendering synchronously in the middle of an event gives up the
|
|
38923
|
+
* batching that makes several state changes one commit.
|
|
38513
38924
|
*/
|
|
38514
|
-
const
|
|
38515
|
-
|
|
38516
|
-
|
|
38517
|
-
)
|
|
38518
|
-
const onDocumentPointerDown = (pointerDownEvent) => {
|
|
38519
|
-
if (pointerDownEvent.button !== 0) {
|
|
38520
|
-
return;
|
|
38521
|
-
}
|
|
38522
|
-
if (openedDuringThisPress(openController)) {
|
|
38523
|
-
// The release of the press that opened it is not somebody dismissing it
|
|
38524
|
-
// (see openedDuringThisPress).
|
|
38525
|
-
return;
|
|
38526
|
-
}
|
|
38527
|
-
// The press landed inside another popup: that is a press on what is in
|
|
38528
|
-
// front, not outside. Asking the target where it lives rather than asking
|
|
38529
|
-
// this popup whether it was pushed — a popup in front does not have to be
|
|
38530
|
-
// one this popup knows about. In front means neither inside nor around: a
|
|
38531
|
-
// popup nested in this one falls to the containment check below as the
|
|
38532
|
-
// inside press it is, and one this popup opened INSIDE (a bubble rendered
|
|
38533
|
-
// in a sheet's content) is around it, so what that popup holds is outside
|
|
38534
|
-
// this one exactly like the rest of the page.
|
|
38535
|
-
const popupUnderPointer = pointerDownEvent.target.closest?.(
|
|
38536
|
-
`[navi-control="dialog"], [navi-control="popover"]`,
|
|
38537
|
-
);
|
|
38538
|
-
if (
|
|
38539
|
-
popupUnderPointer &&
|
|
38540
|
-
popupUnderPointer !== popupEl &&
|
|
38541
|
-
!popupEl.contains(popupUnderPointer) &&
|
|
38542
|
-
!popupUnderPointer.contains(popupEl)
|
|
38543
|
-
) {
|
|
38544
|
-
return;
|
|
38545
|
-
}
|
|
38546
|
-
if (
|
|
38547
|
-
popupEl.id &&
|
|
38548
|
-
pointerDownEvent.target.closest?.(
|
|
38549
|
-
`[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupEl.id)}"]`,
|
|
38550
|
-
)
|
|
38551
|
-
) {
|
|
38552
|
-
return;
|
|
38553
|
-
}
|
|
38554
|
-
// Real DOM containment wins over the rectangle below — an element
|
|
38555
|
-
// genuinely inside the popup (`overflow: visible`, a negative margin, an
|
|
38556
|
-
// absolutely-positioned child) can be painted outside its border box, and
|
|
38557
|
-
// a press there is not outside just because its coordinates are. The
|
|
38558
|
-
// popup element itself is excluded (contains() answers true for it): a
|
|
38559
|
-
// press on a modal dialog's native backdrop reports the dialog as its
|
|
38560
|
-
// target, there being no `::backdrop` node to be one, so treating that as
|
|
38561
|
-
// contained would keep the rectangle from ever running.
|
|
38562
|
-
if (
|
|
38563
|
-
pointerDownEvent.target !== popupEl &&
|
|
38564
|
-
popupEl.contains(pointerDownEvent.target)
|
|
38565
|
-
) {
|
|
38566
|
-
return;
|
|
38567
|
-
}
|
|
38568
|
-
const rect = popupEl.getBoundingClientRect();
|
|
38569
|
-
const isOutside =
|
|
38570
|
-
pointerDownEvent.clientX < rect.left ||
|
|
38571
|
-
pointerDownEvent.clientX > rect.right ||
|
|
38572
|
-
pointerDownEvent.clientY < rect.top ||
|
|
38573
|
-
pointerDownEvent.clientY > rect.bottom;
|
|
38574
|
-
if (!isOutside) {
|
|
38575
|
-
return;
|
|
38576
|
-
}
|
|
38577
|
-
openController.requestClose(pointerDownEvent, {
|
|
38578
|
-
isCancel: pressOutside === "cancel",
|
|
38579
|
-
});
|
|
38580
|
-
};
|
|
38581
|
-
document.addEventListener(pressEventType, onDocumentPointerDown, {
|
|
38582
|
-
capture: true,
|
|
38583
|
-
});
|
|
38584
|
-
return () => {
|
|
38585
|
-
document.removeEventListener(pressEventType, onDocumentPointerDown, {
|
|
38586
|
-
capture: true,
|
|
38587
|
-
});
|
|
38925
|
+
const flushSyncRendering = (fn) => {
|
|
38926
|
+
const debounceRenderingPrevious = options.debounceRendering;
|
|
38927
|
+
options.debounceRendering = (drainRenderQueue) => {
|
|
38928
|
+
drainRenderQueue();
|
|
38588
38929
|
};
|
|
38930
|
+
try {
|
|
38931
|
+
fn();
|
|
38932
|
+
} finally {
|
|
38933
|
+
options.debounceRendering = debounceRenderingPrevious;
|
|
38934
|
+
}
|
|
38589
38935
|
};
|
|
38590
38936
|
|
|
38591
38937
|
/**
|
|
@@ -65577,6 +65923,13 @@ const css$D = /* css */`
|
|
|
65577
65923
|
* is a `Popover`-level answer only for `"close"`/`"cancel"`; `"capture"` has
|
|
65578
65924
|
* nothing left to absorb without a wall, and `"ignore"` renders no wall anyway.
|
|
65579
65925
|
* See docs/popup_backdrop.md.
|
|
65926
|
+
* @param {string} [props.group] - The popover's peers: the triggers carrying
|
|
65927
|
+
* `data-navi-popup-group` with this name. A press on one, while the popover
|
|
65928
|
+
* is open, closes it and opens the peer's popup in that same press — a
|
|
65929
|
+
* menu bar, where pressing another menu switches to it. The wall still
|
|
65930
|
+
* takes the press (the page behind stays out of reach, hover included) and
|
|
65931
|
+
* hands its click to the peer it covered. Without a wall there is nothing
|
|
65932
|
+
* to ask: the press reaches the peer on its own.
|
|
65580
65933
|
* @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="ignore"]
|
|
65581
65934
|
* - `"ignore"` (default): no backdrop at all, outside presses pass straight
|
|
65582
65935
|
* through. `"close"` closes the popover on an outside press, `"cancel"`
|
|
@@ -65951,6 +66304,7 @@ const usePopoverProps = props => {
|
|
|
65951
66304
|
// which is a sibling element (see this file's top comment).
|
|
65952
66305
|
backdropColor,
|
|
65953
66306
|
backdropFilter,
|
|
66307
|
+
group,
|
|
65954
66308
|
scrollCapture,
|
|
65955
66309
|
focusCapture,
|
|
65956
66310
|
// "auto" (default) → the popover follows its content. "frozen" → measured
|
|
@@ -66596,13 +66950,21 @@ const usePopoverProps = props => {
|
|
|
66596
66950
|
}
|
|
66597
66951
|
if (backdropEl) {
|
|
66598
66952
|
backdropEl.setAttribute("aria-expanded", "false");
|
|
66599
|
-
disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent,
|
|
66953
|
+
disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent, clickEvent => {
|
|
66600
66954
|
// Set regardless of isTopLayer — see openEffect's own identical
|
|
66601
66955
|
// comment for why hidePopover() alone isn't reliably sufficient.
|
|
66602
66956
|
backdropEl.setAttribute("navi-hidden", "");
|
|
66603
66957
|
if (isTopLayer) {
|
|
66604
66958
|
backdropEl.hidePopover();
|
|
66605
66959
|
}
|
|
66960
|
+
if (group && clickEvent) {
|
|
66961
|
+
handWallClickToPeer(clickEvent, {
|
|
66962
|
+
wallEl: backdropEl,
|
|
66963
|
+
popupEl: popoverEl,
|
|
66964
|
+
anchorEl: anchorElement,
|
|
66965
|
+
group
|
|
66966
|
+
});
|
|
66967
|
+
}
|
|
66606
66968
|
});
|
|
66607
66969
|
}
|
|
66608
66970
|
restoreFocus?.(closeEvent);
|
|
@@ -67024,6 +67386,10 @@ const Popup = props => {
|
|
|
67024
67386
|
scrollCapture,
|
|
67025
67387
|
positionAreaFixed,
|
|
67026
67388
|
positionAreaWhenAnchorIsInvalid,
|
|
67389
|
+
// A top-layer dialog's wall is its native ::backdrop, which goes with the
|
|
67390
|
+
// dialog on the press that closes it: no click is left on it to hand to a
|
|
67391
|
+
// peer (see handWallClickToPeer).
|
|
67392
|
+
group,
|
|
67027
67393
|
// Dialog-only, destructured out for the same reason: forwarded below only
|
|
67028
67394
|
// in the dialog branch, so they never reach the popover element.
|
|
67029
67395
|
dockedOnSmallTouchScreen,
|
|
@@ -67073,6 +67439,7 @@ const Popup = props => {
|
|
|
67073
67439
|
positionAreaWhenAnchorIsInvalid: positionAreaWhenAnchorIsInvalid,
|
|
67074
67440
|
scrollCapture: scrollCapture === "popover" || scrollCapture,
|
|
67075
67441
|
positionAreaFixed: positionAreaFixed,
|
|
67442
|
+
group: group,
|
|
67076
67443
|
className: withPropsClassName("navi_popup", className),
|
|
67077
67444
|
expand: expand,
|
|
67078
67445
|
expandX: expandX,
|
|
@@ -67976,13 +68343,18 @@ const PickerContentInsidePopup = props => {
|
|
|
67976
68343
|
calloutIcon,
|
|
67977
68344
|
calloutCloseButton,
|
|
67978
68345
|
calloutCloseByScroll,
|
|
68346
|
+
popupGroup,
|
|
67979
68347
|
...rest
|
|
67980
68348
|
} = props;
|
|
67981
68349
|
const isPopover = mode === "popover";
|
|
67982
68350
|
const isCallout = mode === "callout";
|
|
67983
68351
|
return jsx(Next, {
|
|
67984
68352
|
"aria-haspopup": isPopover ? "listbox" : "dialog",
|
|
67985
|
-
"navi-popover-mode": isPopover ? popoverMode : undefined
|
|
68353
|
+
"navi-popover-mode": isPopover ? popoverMode : undefined
|
|
68354
|
+
// On the trigger, whatever the mode: a peer's popup reads it off the
|
|
68355
|
+
// press that lands here (see `group` in callout.js).
|
|
68356
|
+
,
|
|
68357
|
+
"data-navi-popup-group": popupGroup,
|
|
67986
68358
|
...rest,
|
|
67987
68359
|
// On popupProps already (see the picker's popup assembly); they mean
|
|
67988
68360
|
// nothing to the picker element.
|
|
@@ -68018,7 +68390,12 @@ const PickerContentInsidePopup = props => {
|
|
|
68018
68390
|
status: calloutStatus,
|
|
68019
68391
|
icon: calloutIcon,
|
|
68020
68392
|
closeButton: calloutCloseButton,
|
|
68021
|
-
closeByScroll: calloutCloseByScroll
|
|
68393
|
+
closeByScroll: calloutCloseByScroll
|
|
68394
|
+
// Left unsaid, the callout's own default: no backdrop. It dims
|
|
68395
|
+
// nothing, so the page looks as pressable as it did before.
|
|
68396
|
+
,
|
|
68397
|
+
backdrop: backdrop,
|
|
68398
|
+
group: popupGroup,
|
|
68022
68399
|
children: jsx(PopupModeContext.Provider, {
|
|
68023
68400
|
value: mode,
|
|
68024
68401
|
children: children
|
|
@@ -68055,6 +68432,7 @@ const PickerContentInsidePopup = props => {
|
|
|
68055
68432
|
backdropVariant: backdropVariant,
|
|
68056
68433
|
backdropColor: backdropColor,
|
|
68057
68434
|
backdropFilter: backdropFilter,
|
|
68435
|
+
group: popupGroup,
|
|
68058
68436
|
focusCapture: isPopover ? focusCapture : undefined,
|
|
68059
68437
|
expand: isPopover ? undefined : dialogExpand,
|
|
68060
68438
|
expandX: isPopover ? undefined : dialogExpandX,
|
|
@@ -68114,6 +68492,8 @@ const PickerCalloutPopup = ({
|
|
|
68114
68492
|
icon,
|
|
68115
68493
|
closeButton,
|
|
68116
68494
|
closeByScroll,
|
|
68495
|
+
backdrop,
|
|
68496
|
+
group,
|
|
68117
68497
|
onnavi_request_open,
|
|
68118
68498
|
onnavi_request_close,
|
|
68119
68499
|
onnavi_request_confirm,
|
|
@@ -68155,6 +68535,8 @@ const PickerCalloutPopup = ({
|
|
|
68155
68535
|
icon,
|
|
68156
68536
|
closeButton,
|
|
68157
68537
|
closeByScroll,
|
|
68538
|
+
backdrop,
|
|
68539
|
+
group,
|
|
68158
68540
|
anchorElement,
|
|
68159
68541
|
// The request, chained to the press that made it: the callout reads the
|
|
68160
68542
|
// mousedown off it to wait for the release before listening for a click
|
|
@@ -70321,7 +70703,13 @@ const ListRowContext = createContext(null);
|
|
|
70321
70703
|
// row takes its place in the collection by slot: the place is then the list's
|
|
70322
70704
|
// to move, and the row's to follow — see list_rows.js.
|
|
70323
70705
|
const ListSlotContext = createContext(null);
|
|
70324
|
-
const css$x = /* css */`@
|
|
70706
|
+
const css$x = /* css */`@property --list-group-label-height {
|
|
70707
|
+
syntax: "<length>";
|
|
70708
|
+
inherits: false;
|
|
70709
|
+
initial-value: 0;
|
|
70710
|
+
}
|
|
70711
|
+
|
|
70712
|
+
@layer navi {
|
|
70325
70713
|
.navi_list_container {
|
|
70326
70714
|
--list-outline-width: 1px;
|
|
70327
70715
|
--list-border-radius: 4px;
|
|
@@ -70720,6 +71108,7 @@ const css$x = /* css */`@layer navi {
|
|
|
70720
71108
|
}
|
|
70721
71109
|
|
|
70722
71110
|
& .navi_list_item_group_list {
|
|
71111
|
+
--list-group-label-height: inherit;
|
|
70723
71112
|
flex-direction: column;
|
|
70724
71113
|
width: 100%;
|
|
70725
71114
|
margin: 0;
|
|
@@ -70727,8 +71116,12 @@ const css$x = /* css */`@layer navi {
|
|
|
70727
71116
|
list-style: none;
|
|
70728
71117
|
display: flex;
|
|
70729
71118
|
|
|
71119
|
+
& > .navi_list_item {
|
|
71120
|
+
--list-group-label-height: inherit;
|
|
71121
|
+
}
|
|
71122
|
+
|
|
70730
71123
|
& .navi_list_item {
|
|
70731
|
-
scroll-margin-top: calc(var(--x-list-scroll-spacing-top) + var(--list-group-label-height
|
|
71124
|
+
scroll-margin-top: calc(var(--x-list-scroll-spacing-top) + var(--list-group-label-height));
|
|
70732
71125
|
}
|
|
70733
71126
|
}
|
|
70734
71127
|
|
|
@@ -70880,9 +71273,17 @@ const ListUI = props => {
|
|
|
70880
71273
|
// why not an effect), and the floor of 30 is its: it protects the scrolling
|
|
70881
71274
|
// window, not a picture nobody has seen yet.
|
|
70882
71275
|
const {
|
|
70883
|
-
initial:
|
|
71276
|
+
initial: initialRenderBudgetProp,
|
|
70884
71277
|
after: renderBudgetAfterPaint
|
|
70885
71278
|
} = resolveRenderBudget(renderBudgetProp);
|
|
71279
|
+
// Opening on a position that says how many rows were on screen from its row
|
|
71280
|
+
// on (the way back to where the list was, see reportPosition): that is what
|
|
71281
|
+
// the first picture shows, whatever `initial` guessed — with the row above
|
|
71282
|
+
// when it stood partly in view. The rest waits for the paint like any
|
|
71283
|
+
// `initial`: a page coming back in a route transition builds it inside the
|
|
71284
|
+
// update callback, and every row below the fold delays the movement.
|
|
71285
|
+
const openingPosition = scrolled ?? defaultScrolled;
|
|
71286
|
+
const initialRenderBudget = openingPosition && typeof openingPosition === "object" && typeof openingPosition.visibleCount === "number" ? openingPosition.visibleCount + (openingPosition.offset > 0 ? 1 : 0) : initialRenderBudgetProp;
|
|
70886
71287
|
let renderBudget = renderBudgetAfterPaint;
|
|
70887
71288
|
if (renderBudget < 30 && !renderBudgetSkipCheck) {
|
|
70888
71289
|
console.warn(`List: renderBudget=${renderBudget} is too low. A renderBudget below 30 is not supported: on large screens or when the list grows, items outside the window would appear as blank space instead of rendered content. Use a value of at least 30, or omit the prop to use the default (${RENDER_BUDGET_DEFAULT}).`);
|
|
@@ -71345,11 +71746,23 @@ const useListScrollSync = ({
|
|
|
71345
71746
|
horizontal
|
|
71346
71747
|
});
|
|
71347
71748
|
};
|
|
71749
|
+
|
|
71750
|
+
// How many rows the window keeps above the row the list is held at. Around it
|
|
71751
|
+
// for the scrolling to come, from the first paint on; before, the window is
|
|
71752
|
+
// the first picture's, and a picture of the list opening on a row shows that
|
|
71753
|
+
// row and what is below it — with the one above when a remembered position
|
|
71754
|
+
// says it stood partly in view.
|
|
71755
|
+
const rowsAboveOpening = (openAt, windowSize) => {
|
|
71756
|
+
if (renderBudget !== renderBudgetSteady) {
|
|
71757
|
+
return openAt && typeof openAt === "object" && openAt.offset > 0 ? 1 : 0;
|
|
71758
|
+
}
|
|
71759
|
+
return Math.floor(windowSize / 2);
|
|
71760
|
+
};
|
|
71348
71761
|
const [renderWindow, setRenderWindow] = useState(() => {
|
|
71349
71762
|
// Opening somewhere else than the beginning starts by framing there: the
|
|
71350
71763
|
// rows the list will draw are the rows it will ask for.
|
|
71351
71764
|
const openAt = scrolled ?? defaultScrolled;
|
|
71352
|
-
const start = typeof openAt === "number" ? openAt -
|
|
71765
|
+
const start = typeof openAt === "number" ? openAt - rowsAboveOpening(openAt, renderBudget) : 0;
|
|
71353
71766
|
const startClamped = start < 0 ? 0 : start;
|
|
71354
71767
|
return {
|
|
71355
71768
|
start: startClamped,
|
|
@@ -71422,8 +71835,12 @@ const useListScrollSync = ({
|
|
|
71422
71835
|
// Held somewhere it has not reached yet: what the window frames right now
|
|
71423
71836
|
// is not what it will frame, so nothing should be fetched for it.
|
|
71424
71837
|
listRows.holdPending = scrolledWanted !== "start" && scrolledWanted !== undefined;
|
|
71838
|
+
// Zero until the runs have rendered — they count their rows as they do,
|
|
71839
|
+
// after the list — which is not a collection fitting in the window: it is
|
|
71840
|
+
// one whose end is not known yet, and the window then frames the row the
|
|
71841
|
+
// list opens on, already in the first commit.
|
|
71425
71842
|
const total = listRows.totalSignal.peek();
|
|
71426
|
-
if (total <= renderBudget) {
|
|
71843
|
+
if (total > 0 && total <= renderBudget) {
|
|
71427
71844
|
// The whole collection is what the list draws: wherever in it the list is
|
|
71428
71845
|
// held, the window is already its place. Nowhere to move to means nothing
|
|
71429
71846
|
// to wait for — a hold left standing here is a list that never asks for
|
|
@@ -71431,21 +71848,21 @@ const useListScrollSync = ({
|
|
|
71431
71848
|
listRows.holdPending = false;
|
|
71432
71849
|
return;
|
|
71433
71850
|
}
|
|
71434
|
-
const
|
|
71851
|
+
const above = rowsAboveOpening(scrolledWanted, renderBudget);
|
|
71435
71852
|
let wantedStart = null;
|
|
71436
71853
|
if (scrolledWanted === "end") {
|
|
71437
71854
|
wantedStart = total - renderBudget;
|
|
71438
71855
|
} else if (typeof scrolledWanted === "number") {
|
|
71439
|
-
wantedStart = scrolledWanted -
|
|
71856
|
+
wantedStart = scrolledWanted - above;
|
|
71440
71857
|
} else if (scrolledWanted && scrolledWanted.id !== undefined) {
|
|
71441
71858
|
const rowIndex = listRows.locateRow(scrolledWanted.id);
|
|
71442
71859
|
if (rowIndex !== null) {
|
|
71443
|
-
wantedStart = rowIndex -
|
|
71860
|
+
wantedStart = rowIndex - above;
|
|
71444
71861
|
} else if (typeof scrolledWanted.index === "number") {
|
|
71445
71862
|
// The row has not come back yet, but where it stood is known: near
|
|
71446
71863
|
// enough to frame, and to put the scrollbar roughly where it will end
|
|
71447
71864
|
// up rather than at the top.
|
|
71448
|
-
wantedStart = scrolledWanted.index -
|
|
71865
|
+
wantedStart = scrolledWanted.index - above;
|
|
71449
71866
|
}
|
|
71450
71867
|
}
|
|
71451
71868
|
if (wantedStart === null) {
|
|
@@ -71454,12 +71871,12 @@ const useListScrollSync = ({
|
|
|
71454
71871
|
// back — the run asks for it by name (see useRequestMissing).
|
|
71455
71872
|
return;
|
|
71456
71873
|
}
|
|
71874
|
+
if (total > 0 && wantedStart + renderBudget > total) {
|
|
71875
|
+
wantedStart = total - renderBudget;
|
|
71876
|
+
}
|
|
71457
71877
|
if (wantedStart < 0) {
|
|
71458
71878
|
wantedStart = 0;
|
|
71459
71879
|
}
|
|
71460
|
-
if (wantedStart + renderBudget > total) {
|
|
71461
|
-
wantedStart = total - renderBudget;
|
|
71462
|
-
}
|
|
71463
71880
|
const {
|
|
71464
71881
|
start,
|
|
71465
71882
|
end
|
|
@@ -71794,8 +72211,8 @@ const useListScrollSync = ({
|
|
|
71794
72211
|
end
|
|
71795
72212
|
} = renderWindowRef.current;
|
|
71796
72213
|
if (rowIndex < start || rowIndex >= end) {
|
|
71797
|
-
const
|
|
71798
|
-
const wantedStart = rowIndex -
|
|
72214
|
+
const above = rowsAboveOpening(scrolledWanted, end - start);
|
|
72215
|
+
const wantedStart = rowIndex - above < 0 ? 0 : rowIndex - above;
|
|
71799
72216
|
updateRenderWindow(wantedStart, wantedStart + (end - start), `opening on row ${scrolledWanted.id}`);
|
|
71800
72217
|
return;
|
|
71801
72218
|
}
|
|
@@ -71841,6 +72258,15 @@ const useListScrollSync = ({
|
|
|
71841
72258
|
}
|
|
71842
72259
|
};
|
|
71843
72260
|
useLayoutEffect(placeWhereHeld);
|
|
72261
|
+
// Held on a row of a list scrolling the document: where the document goes is
|
|
72262
|
+
// this list's to say, measured on the row, not the url's offset in pixels
|
|
72263
|
+
// (see holdDocumentScroll).
|
|
72264
|
+
useLayoutEffect(() => {
|
|
72265
|
+
if (!heldSomewhere || getScroller() !== document.scrollingElement) {
|
|
72266
|
+
return undefined;
|
|
72267
|
+
}
|
|
72268
|
+
return holdDocumentScroll();
|
|
72269
|
+
}, [heldSomewhere, scrollerElResolved]);
|
|
71844
72270
|
// What to do when the list's own geometry moves under it, kept fresh for the
|
|
71845
72271
|
// observer below (which is installed once).
|
|
71846
72272
|
const onGeometryChangeRef = useRef(null);
|
|
@@ -71883,31 +72309,40 @@ const useListScrollSync = ({
|
|
|
71883
72309
|
// Where the list was at the last thing that moved it. Kept whether anyone
|
|
71884
72310
|
// asked for it or not: it is what a resize needs to put things back.
|
|
71885
72311
|
const positionRef = useRef(null);
|
|
72312
|
+
const captureListAnchor = countVisible => captureScrollAnchor({
|
|
72313
|
+
scrollerEl: getScroller(),
|
|
72314
|
+
listEl: getListEl(),
|
|
72315
|
+
items: listRows.visibleItemsSignal.peek(),
|
|
72316
|
+
horizontal,
|
|
72317
|
+
countVisible
|
|
72318
|
+
});
|
|
72319
|
+
// Said from where the row lands on its own, and with `visibleCount`: the
|
|
72320
|
+
// rows on screen from that one on, which is what a list given this position
|
|
72321
|
+
// back draws first (see ListUI).
|
|
72322
|
+
const toScrolledPosition = position => {
|
|
72323
|
+
const rowEl = findRowElement(getListEl(), position.id);
|
|
72324
|
+
return {
|
|
72325
|
+
id: position.id,
|
|
72326
|
+
index: position.index,
|
|
72327
|
+
offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal),
|
|
72328
|
+
visibleCount: position.visibleCount
|
|
72329
|
+
};
|
|
72330
|
+
};
|
|
71886
72331
|
const reportPosition = () => {
|
|
71887
72332
|
if (!ref.current) {
|
|
71888
72333
|
return;
|
|
71889
72334
|
}
|
|
71890
|
-
const
|
|
71891
|
-
|
|
71892
|
-
|
|
71893
|
-
items: listRows.visibleItemsSignal.peek(),
|
|
71894
|
-
horizontal
|
|
71895
|
-
});
|
|
72335
|
+
const remember = rememberScrollRef.current;
|
|
72336
|
+
const onScrolledChange = onScrolledChangeRef.current;
|
|
72337
|
+
const position = captureListAnchor(remember || Boolean(onScrolledChange));
|
|
71896
72338
|
if (!position) {
|
|
71897
72339
|
return;
|
|
71898
72340
|
}
|
|
71899
72341
|
positionRef.current = position;
|
|
71900
|
-
const remember = rememberScrollRef.current;
|
|
71901
|
-
const onScrolledChange = onScrolledChangeRef.current;
|
|
71902
72342
|
if (!remember && !onScrolledChange) {
|
|
71903
72343
|
return;
|
|
71904
72344
|
}
|
|
71905
|
-
const
|
|
71906
|
-
const scrolledNow = {
|
|
71907
|
-
id: position.id,
|
|
71908
|
-
index: position.index,
|
|
71909
|
-
offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal)
|
|
71910
|
-
};
|
|
72345
|
+
const scrolledNow = toScrolledPosition(position);
|
|
71911
72346
|
if (remember) {
|
|
71912
72347
|
rememberScrollerPosition(listId, scrolledNow);
|
|
71913
72348
|
}
|
|
@@ -71915,6 +72350,20 @@ const useListScrollSync = ({
|
|
|
71915
72350
|
onScrolledChange(scrolledNow);
|
|
71916
72351
|
}
|
|
71917
72352
|
};
|
|
72353
|
+
// Nothing scrolled, yet what is on screen changed: the list has just been
|
|
72354
|
+
// laid out, its rows have arrived, one of them grew. A list read where it
|
|
72355
|
+
// opened would otherwise come back with nothing kept — opening again as a
|
|
72356
|
+
// fresh arrival does, all of its window drawn in the first commit instead of
|
|
72357
|
+
// the rows that were on screen. Kept, and told to nobody: nobody scrolled.
|
|
72358
|
+
const rememberPosition = () => {
|
|
72359
|
+
if (!rememberScrollRef.current || !ref.current) {
|
|
72360
|
+
return;
|
|
72361
|
+
}
|
|
72362
|
+
const position = captureListAnchor(true);
|
|
72363
|
+
if (position) {
|
|
72364
|
+
rememberScrollerPosition(listId, toScrolledPosition(position));
|
|
72365
|
+
}
|
|
72366
|
+
};
|
|
71918
72367
|
// Leaving: with its page, or alone (see forgetScrollerUnlessPageLeft).
|
|
71919
72368
|
useLayoutEffect(() => {
|
|
71920
72369
|
return () => {
|
|
@@ -71940,6 +72389,7 @@ const useListScrollSync = ({
|
|
|
71940
72389
|
// list which has outgrown a bounded ancestor stops holding on to the
|
|
71941
72390
|
// page.
|
|
71942
72391
|
resolveScroller();
|
|
72392
|
+
rememberPosition();
|
|
71943
72393
|
// Two things resize here, and they call for opposite answers. The LIST
|
|
71944
72394
|
// growing is its own content settling: only a list holding itself
|
|
71945
72395
|
// somewhere cares (the end it aims at has moved), and a list the user is
|
|
@@ -72632,12 +73082,15 @@ const getRowName = rowEl => rowEl.getAttribute("navi-list-item-real");
|
|
|
72632
73082
|
// the list is rebuilt around it. Read off the rows' own boxes, not by
|
|
72633
73083
|
// hit-testing the screen: a scrolling list takes its rows out of hit-testing
|
|
72634
73084
|
// (see the navi-scrolling rule in the css above), and the scroll event is
|
|
72635
|
-
// precisely when this is asked.
|
|
73085
|
+
// precisely when this is asked. `countVisible` adds how many rows are on
|
|
73086
|
+
// screen from that one on: what a list coming back there has to draw before
|
|
73087
|
+
// anything else (see the first paint in ListUI).
|
|
72636
73088
|
const captureScrollAnchor = ({
|
|
72637
73089
|
scrollerEl,
|
|
72638
73090
|
listEl,
|
|
72639
73091
|
items,
|
|
72640
|
-
horizontal
|
|
73092
|
+
horizontal,
|
|
73093
|
+
countVisible
|
|
72641
73094
|
}) => {
|
|
72642
73095
|
if (!scrollerEl || !listEl) {
|
|
72643
73096
|
return null;
|
|
@@ -72673,6 +73126,9 @@ const captureScrollAnchor = ({
|
|
|
72673
73126
|
offset
|
|
72674
73127
|
};
|
|
72675
73128
|
if (offset >= 0) {
|
|
73129
|
+
if (countVisible) {
|
|
73130
|
+
anchor.visibleCount = countRowsStartingBefore(rowEls, i, range.to, horizontal);
|
|
73131
|
+
}
|
|
72676
73132
|
return anchor;
|
|
72677
73133
|
}
|
|
72678
73134
|
// The row under the top of the view starts above it. Good enough to hold
|
|
@@ -72681,10 +73137,26 @@ const captureScrollAnchor = ({
|
|
|
72681
73137
|
// top" reads, and can be drawn. Keep looking; this one is the fallback.
|
|
72682
73138
|
if (!fallbackAnchor) {
|
|
72683
73139
|
fallbackAnchor = anchor;
|
|
73140
|
+
if (countVisible) {
|
|
73141
|
+
anchor.visibleCount = countRowsStartingBefore(rowEls, i, range.to, horizontal);
|
|
73142
|
+
}
|
|
72684
73143
|
}
|
|
72685
73144
|
}
|
|
72686
73145
|
return fallbackAnchor;
|
|
72687
73146
|
};
|
|
73147
|
+
// How many of the rows from `fromIndex` on start before `to` along the scroll
|
|
73148
|
+
// axis — the ones on screen, when `to` is where the view ends.
|
|
73149
|
+
const countRowsStartingBefore = (rowEls, fromIndex, to, horizontal) => {
|
|
73150
|
+
let index = fromIndex;
|
|
73151
|
+
while (index < rowEls.length) {
|
|
73152
|
+
const rect = rowEls[index].getBoundingClientRect();
|
|
73153
|
+
if ((horizontal ? rect.left : rect.top) >= to) {
|
|
73154
|
+
break;
|
|
73155
|
+
}
|
|
73156
|
+
index++;
|
|
73157
|
+
}
|
|
73158
|
+
return index - fromIndex;
|
|
73159
|
+
};
|
|
72688
73160
|
// The part of the list that is on screen, along the scrolling axis. Both edges
|
|
72689
73161
|
// matter: the scroller may be larger than the list (scroller="parent") as well
|
|
72690
73162
|
// as smaller (the list scrolls inside its own box).
|
|
@@ -75040,7 +75512,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
75040
75512
|
* are skeletons, the others hold their room. `0` says the list is already
|
|
75041
75513
|
* known to be empty: the empty `fallback` shows right away rather than an
|
|
75042
75514
|
* empty frame.
|
|
75043
|
-
* @param {"start"|"end"|number|{id: string, offset?: number}} [props.defaultScrolled="start"]
|
|
75515
|
+
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.defaultScrolled="start"]
|
|
75044
75516
|
* Where the list opens, after which the user owns the scroll — unless it is
|
|
75045
75517
|
* being come back to (see `scrollResetOnNavigation`). `"end"` is a
|
|
75046
75518
|
* thread read backwards — the last rows are the ones to show, and the ones
|
|
@@ -75052,8 +75524,10 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
75052
75524
|
* the screen it was saved on. `offset: 0` is where a `scrollIntoView()` puts
|
|
75053
75525
|
* it — in front of the fixed bar the scroller gives room for, below the
|
|
75054
75526
|
* sticky header and the group label the row lives under — so nothing of that
|
|
75055
|
-
* room has to be restated as a number by whoever asks.
|
|
75056
|
-
*
|
|
75527
|
+
* room has to be restated as a number by whoever asks. The `index` and
|
|
75528
|
+
* `visibleCount` it also hands out say where to aim before the row is found,
|
|
75529
|
+
* and how many rows to draw before the first paint (see `renderBudget`).
|
|
75530
|
+
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.scrolled]
|
|
75057
75531
|
* The same, but held: the list goes back there every time this changes, even
|
|
75058
75532
|
* after the user has scrolled — the caller owns where the list is (see
|
|
75059
75533
|
* `defaultScrolled` for the uncontrolled form, and `open`/`defaultOpen`
|
|
@@ -75064,12 +75538,12 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
75064
75538
|
* In every form the list holds itself there while it is still finding out
|
|
75065
75539
|
* how many rows there are and how tall one is, and lets go the moment the
|
|
75066
75540
|
* user reaches for the list.
|
|
75067
|
-
* @param {(scrolled: {id: string, index: number, offset: number}) => void} [props.onScrolledChange]
|
|
75541
|
+
* @param {(scrolled: {id: string, index: number, offset: number, visibleCount: number}) => void} [props.onScrolledChange]
|
|
75068
75542
|
* Where the list is, as the user scrolls: the row at the top of the view and
|
|
75069
75543
|
* how far below the place a row lands on its own (see `defaultScrolled`) it
|
|
75070
|
-
* starts. Keep it
|
|
75071
|
-
* later through `scrolled`/`defaultScrolled` — an index
|
|
75072
|
-
* rows get inserted while a list is being read.
|
|
75544
|
+
* starts, and how many rows are on screen from that one on. Keep it whole to
|
|
75545
|
+
* come back to it later through `scrolled`/`defaultScrolled` — an index alone
|
|
75546
|
+
* would not do, since rows get inserted while a list is being read.
|
|
75073
75547
|
* @param {number|{initial: number, after: number}} [props.renderBudget=100]
|
|
75074
75548
|
* How many rows of a `<List.Items>` run are in the DOM at once: the render
|
|
75075
75549
|
* window, which slides as the user scrolls while fillers hold the room of
|
|
@@ -75081,11 +75555,15 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
75081
75555
|
* once, with room for that lookahead — the list warns below 30, and when a
|
|
75082
75556
|
* budget leaves fewer than two rows beyond the screen.
|
|
75083
75557
|
*
|
|
75084
|
-
* `{ initial, after }` for a list
|
|
75085
|
-
*
|
|
75086
|
-
*
|
|
75087
|
-
*
|
|
75088
|
-
*
|
|
75558
|
+
* `{ initial, after }` for a list whose first picture is taken as it is
|
|
75559
|
+
* built — drawn in the click that opens a popup, or in the update callback
|
|
75560
|
+
* of a route transition bringing its page back: `initial` rows in the commit
|
|
75561
|
+
* the browser paints first — about what a phone screen shows — counted from
|
|
75562
|
+
* the row the list opens on, and `after` from the paint on, around it, where
|
|
75563
|
+
* the floor of 30 applies. Opening on a position that says how many rows were
|
|
75564
|
+
* on screen (`visibleCount`, see `onScrolledChange`) draws those first,
|
|
75565
|
+
* whatever `initial` says. The runs ask their source for `after` rows from
|
|
75566
|
+
* the start, so the first picture costs no second request.
|
|
75089
75567
|
* @param {number} [props.virtualItemSize]
|
|
75090
75568
|
* The size of one row along the scroll axis, in px, when every row has the
|
|
75091
75569
|
* same: what the fillers are sized with and what a scroll position is
|