@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/dev/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, createEventGroupLogger, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet,
|
|
6
|
+
import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, createEventGroupLogger, 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, getVirtualKeyboardOverlayHeight, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange } 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";
|
|
@@ -11254,6 +11254,28 @@ const suspendScrollRecording = () => {
|
|
|
11254
11254
|
};
|
|
11255
11255
|
};
|
|
11256
11256
|
|
|
11257
|
+
// A list scrolling the document that opens on a row puts the document where
|
|
11258
|
+
// that row is, by measuring it (see placeWhereHeld in list.jsx). The offset kept
|
|
11259
|
+
// for the url is pixels of the page as it was drawn — and a list draws a window
|
|
11260
|
+
// of its rows and holds the room of the others with fillers of an estimated
|
|
11261
|
+
// height, so the same pixels now fall on other rows. Put back after the list
|
|
11262
|
+
// has placed itself, they would undo it, in the very picture a route transition
|
|
11263
|
+
// takes. So the list holds the document while it is placing itself, and the
|
|
11264
|
+
// url's offset is not put back meanwhile. Counted: two lists can share one
|
|
11265
|
+
// document.
|
|
11266
|
+
let documentHoldCount = 0;
|
|
11267
|
+
const holdDocumentScroll = () => {
|
|
11268
|
+
documentHoldCount++;
|
|
11269
|
+
let released = false;
|
|
11270
|
+
return () => {
|
|
11271
|
+
if (released) {
|
|
11272
|
+
return;
|
|
11273
|
+
}
|
|
11274
|
+
released = true;
|
|
11275
|
+
documentHoldCount--;
|
|
11276
|
+
};
|
|
11277
|
+
};
|
|
11278
|
+
|
|
11257
11279
|
let installed = false;
|
|
11258
11280
|
const installScrollRestoration = () => {
|
|
11259
11281
|
if (installed) {
|
|
@@ -11288,6 +11310,9 @@ const installScrollRestoration = () => {
|
|
|
11288
11310
|
if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
|
|
11289
11311
|
const stopListening = observeRouteRender(() => {
|
|
11290
11312
|
stopListening();
|
|
11313
|
+
if (documentHoldCount) {
|
|
11314
|
+
return;
|
|
11315
|
+
}
|
|
11291
11316
|
scrollTo(positionOnLoad);
|
|
11292
11317
|
});
|
|
11293
11318
|
}
|
|
@@ -11302,6 +11327,11 @@ const restoreScrollPosition = (url) => {
|
|
|
11302
11327
|
if (!position) {
|
|
11303
11328
|
return false;
|
|
11304
11329
|
}
|
|
11330
|
+
if (documentHoldCount) {
|
|
11331
|
+
// There is a place to go back to, and the list holding the document is
|
|
11332
|
+
// putting it back (see holdDocumentScroll).
|
|
11333
|
+
return true;
|
|
11334
|
+
}
|
|
11305
11335
|
scrollTo(position);
|
|
11306
11336
|
return true;
|
|
11307
11337
|
};
|
|
@@ -15851,224 +15881,846 @@ const useOpenControllerByProps = (props, name) => {
|
|
|
15851
15881
|
};
|
|
15852
15882
|
|
|
15853
15883
|
/**
|
|
15854
|
-
*
|
|
15855
|
-
*
|
|
15884
|
+
* Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
|
|
15885
|
+
* (non-top-layer) renderers — operate on a plain DOM element, no knowledge
|
|
15886
|
+
* of which of the two owns it.
|
|
15887
|
+
*/
|
|
15888
|
+
|
|
15889
|
+
|
|
15890
|
+
/**
|
|
15891
|
+
* Whether a visibleRectEffect delivery is one that can have taken height away
|
|
15892
|
+
* from a popup, and so pushed whatever holds focus out of sight:
|
|
15893
|
+
* - "resize": the window/visual viewport settled — which is also how the
|
|
15894
|
+
* on-screen keyboard arrives, overlay or not (window_size.js);
|
|
15895
|
+
* - ELEMENT_SIZE_CHANGE: the popup's own box measured different;
|
|
15896
|
+
* - "focusin": the focus-settled re-measure, for the room that changes with
|
|
15897
|
+
* nothing announcing it (subscribeFocusSettled in window_size.js).
|
|
15856
15898
|
*
|
|
15857
|
-
*
|
|
15858
|
-
*
|
|
15859
|
-
*
|
|
15860
|
-
|
|
15861
|
-
|
|
15862
|
-
|
|
15863
|
-
|
|
15864
|
-
|
|
15899
|
+
* Everything else is a scroll of one kind or another, where nothing got
|
|
15900
|
+
* smaller and scrolling the focused element back would fight the very gesture
|
|
15901
|
+
* that fired it.
|
|
15902
|
+
*/
|
|
15903
|
+
const mayHaveHiddenFocus = (event) => {
|
|
15904
|
+
const type = event?.type;
|
|
15905
|
+
return (
|
|
15906
|
+
type === "resize" || type === ELEMENT_SIZE_CHANGE || type === "focusin"
|
|
15907
|
+
);
|
|
15908
|
+
};
|
|
15909
|
+
|
|
15910
|
+
const scrollportHeightMap = new WeakMap();
|
|
15911
|
+
/**
|
|
15912
|
+
* Scrolls whatever holds focus inside `popupEl` back into view, if the popup
|
|
15913
|
+
* getting shorter has pushed it out.
|
|
15865
15914
|
*
|
|
15866
|
-
*
|
|
15867
|
-
*
|
|
15868
|
-
*
|
|
15869
|
-
*
|
|
15870
|
-
*
|
|
15871
|
-
*
|
|
15915
|
+
* The case this exists for: a field low in the scrolling body of a popup that
|
|
15916
|
+
* also has a footer (box.jsx — with a body, the body is the only thing that
|
|
15917
|
+
* scrolls and the footer is a sibling sitting right under it). Focusing the
|
|
15918
|
+
* field makes the browser scroll it into view, which it does against the
|
|
15919
|
+
* popup's height AT THAT MOMENT; the on-screen keyboard then opens and takes
|
|
15920
|
+
* that height away. The body shrinks, its scrollTop does not move, so the
|
|
15921
|
+
* content slides down relative to the shorter scrollport and the field ends up
|
|
15922
|
+
* past its bottom edge — visually, swallowed by the footer. The browser does
|
|
15923
|
+
* not redo a scroll-into-view it already answered, so this does.
|
|
15872
15924
|
*
|
|
15873
|
-
*
|
|
15874
|
-
*
|
|
15875
|
-
*
|
|
15876
|
-
*
|
|
15877
|
-
* the
|
|
15878
|
-
*
|
|
15925
|
+
* Only what the shrink itself hid, though: with the keyboard up the user reads
|
|
15926
|
+
* the rest of the popup by scrolling the field away — to reach the submit under
|
|
15927
|
+
* it, typically — and the room keeps changing while they do (a keyboard settling
|
|
15928
|
+
* in two steps, a suggestion strip, a browser bar). Answering each of those by
|
|
15929
|
+
* scrolling the field back takes the popup away from wherever they had just
|
|
15930
|
+
* scrolled it, over and over: what they were reading cannot be reached at all
|
|
15931
|
+
* without blurring the field first, and the popup reads as unscrollable. So the
|
|
15932
|
+
* field is brought back only when it was in view before the room shrank, which
|
|
15933
|
+
* one remembered number answers: a resize moves neither scrollTop nor the
|
|
15934
|
+
* element's offset inside the scrolled content, so measuring against the height
|
|
15935
|
+
* the scrollport HAD is measuring the state before the change.
|
|
15879
15936
|
*
|
|
15880
|
-
*
|
|
15881
|
-
*
|
|
15937
|
+
* Scoped to the field's own scroll container (never the page): a popup traps
|
|
15938
|
+
* scrolling precisely so the document underneath cannot move, and a plain
|
|
15939
|
+
* scrollIntoView walks past a container whose scrollbar isn't visible — see
|
|
15940
|
+
* scrollIntoViewScoped's own doc.
|
|
15941
|
+
*
|
|
15942
|
+
* "nearest": the smallest scroll that makes it visible, and none at all when it
|
|
15943
|
+
* already is. Where it lands is the container's own business — a navi scroller
|
|
15944
|
+
* keeps a band free at its edges so a field never comes back glued to one (see
|
|
15945
|
+
* scroll-padding in box.jsx).
|
|
15882
15946
|
*/
|
|
15883
|
-
const
|
|
15884
|
-
|
|
15885
|
-
|
|
15886
|
-
|
|
15887
|
-
|
|
15888
|
-
|
|
15889
|
-
|
|
15890
|
-
|
|
15891
|
-
|
|
15892
|
-
|
|
15893
|
-
|
|
15894
|
-
|
|
15895
|
-
|
|
15947
|
+
const keepFocusedElementVisible = (popupEl) => {
|
|
15948
|
+
const { activeElement } = document;
|
|
15949
|
+
if (!activeElement || activeElement === popupEl) {
|
|
15950
|
+
return;
|
|
15951
|
+
}
|
|
15952
|
+
if (!popupEl.contains(activeElement)) {
|
|
15953
|
+
return;
|
|
15954
|
+
}
|
|
15955
|
+
const scrollContainer = getScrollContainer(activeElement);
|
|
15956
|
+
if (!scrollContainer || !popupEl.contains(scrollContainer)) {
|
|
15957
|
+
// What scrolls the field is outside the popup, which means the page: a
|
|
15958
|
+
// popup holds it still on purpose (trapScrollInside), so there is nothing
|
|
15959
|
+
// here to scroll back.
|
|
15960
|
+
return;
|
|
15961
|
+
}
|
|
15962
|
+
const scrollportHeight = scrollContainer.clientHeight;
|
|
15963
|
+
const scrollportHeightBefore = scrollportHeightMap.get(scrollContainer);
|
|
15964
|
+
scrollportHeightMap.set(scrollContainer, scrollportHeight);
|
|
15965
|
+
if (scrollportHeightBefore !== undefined) {
|
|
15966
|
+
if (scrollportHeight >= scrollportHeightBefore) {
|
|
15967
|
+
// Nothing was taken away, so nothing was hidden by this.
|
|
15968
|
+
return;
|
|
15969
|
+
}
|
|
15970
|
+
if (
|
|
15971
|
+
!isVisibleInScrollport(
|
|
15972
|
+
activeElement,
|
|
15973
|
+
scrollContainer,
|
|
15974
|
+
scrollportHeightBefore,
|
|
15975
|
+
)
|
|
15976
|
+
) {
|
|
15977
|
+
return;
|
|
15896
15978
|
}
|
|
15897
15979
|
}
|
|
15898
|
-
|
|
15899
|
-
|
|
15900
|
-
|
|
15901
|
-
const useCalloutRequestClose = () => {
|
|
15902
|
-
return useContext(CalloutContext)?.requestClose;
|
|
15903
|
-
};
|
|
15904
|
-
const useCalloutElement = () => {
|
|
15905
|
-
return useContext(CalloutContext)?.element;
|
|
15906
|
-
};
|
|
15907
|
-
const renderIntoCallout = (jsx$1, calloutMessageElement, {
|
|
15908
|
-
requestClose,
|
|
15909
|
-
element
|
|
15910
|
-
}) => {
|
|
15911
|
-
const calloutJsx = jsx(CalloutContext.Provider, {
|
|
15912
|
-
value: {
|
|
15913
|
-
requestClose,
|
|
15914
|
-
element
|
|
15915
|
-
},
|
|
15916
|
-
children: jsx$1
|
|
15980
|
+
scrollIntoViewScoped(activeElement, {
|
|
15981
|
+
container: scrollContainer,
|
|
15982
|
+
block: "nearest",
|
|
15917
15983
|
});
|
|
15918
|
-
render(calloutJsx, calloutMessageElement);
|
|
15919
15984
|
};
|
|
15920
15985
|
|
|
15921
|
-
//
|
|
15922
|
-
//
|
|
15923
|
-
//
|
|
15924
|
-
//
|
|
15925
|
-
const
|
|
15926
|
-
|
|
15927
|
-
|
|
15928
|
-
|
|
15929
|
-
const
|
|
15930
|
-
|
|
15931
|
-
|
|
15932
|
-
|
|
15933
|
-
element
|
|
15934
|
-
});
|
|
15986
|
+
// Whether any part of `el` was showing in `container` back when its scrollport
|
|
15987
|
+
// was `scrollportHeight` tall. Both boxes are read now: the container may have
|
|
15988
|
+
// moved as well as shrunk (a centered dialog re-centers itself), and the
|
|
15989
|
+
// difference between the two tops is what that move leaves alone.
|
|
15990
|
+
const isVisibleInScrollport = (el, container, scrollportHeight) => {
|
|
15991
|
+
const elRect = el.getBoundingClientRect();
|
|
15992
|
+
const containerRect = container.getBoundingClientRect();
|
|
15993
|
+
const elTop = elRect.top - containerRect.top + container.scrollTop;
|
|
15994
|
+
const elBottom = elTop + elRect.height;
|
|
15995
|
+
const scrollportTop = container.scrollTop;
|
|
15996
|
+
const scrollportBottom = scrollportTop + scrollportHeight;
|
|
15997
|
+
return elBottom > scrollportTop && elTop < scrollportBottom;
|
|
15935
15998
|
};
|
|
15936
15999
|
|
|
15937
|
-
|
|
15938
|
-
|
|
15939
|
-
|
|
15940
|
-
|
|
15941
|
-
|
|
15942
|
-
|
|
15943
|
-
|
|
15944
|
-
|
|
15945
|
-
|
|
15946
|
-
|
|
15947
|
-
|
|
15948
|
-
|
|
15949
|
-
|
|
16000
|
+
/**
|
|
16001
|
+
* Calls `onSettled` once `el`'s current CSS transition is over — via
|
|
16002
|
+
* `transitionend`, with a safety `setTimeout` fallback matching the longest
|
|
16003
|
+
* `transition-duration`, in case nothing actually transitions or an event is
|
|
16004
|
+
* missed.
|
|
16005
|
+
*
|
|
16006
|
+
* Returns a "cancel" function, so a caller whose instance has been superseded
|
|
16007
|
+
* (a fresh open/close about to set its own state) can keep this stale one from
|
|
16008
|
+
* firing later. Cancelling only stops `onSettled`: undoing whatever the caller
|
|
16009
|
+
* did up front is that fresh call's business, not this one's.
|
|
16010
|
+
*/
|
|
16011
|
+
const whenTransitionSettles = (el, onSettled) => {
|
|
16012
|
+
let settled = false;
|
|
16013
|
+
const onTransitionEnd = (transitionEvent) => {
|
|
16014
|
+
if (transitionEvent.target === el) {
|
|
16015
|
+
finish();
|
|
15950
16016
|
}
|
|
15951
|
-
|
|
15952
|
-
|
|
16017
|
+
};
|
|
16018
|
+
const stopWatching = () => {
|
|
16019
|
+
settled = true;
|
|
16020
|
+
el.removeEventListener("transitionend", onTransitionEnd);
|
|
16021
|
+
clearTimeout(safetyTimeoutId);
|
|
16022
|
+
};
|
|
16023
|
+
const finish = () => {
|
|
16024
|
+
if (settled) {
|
|
16025
|
+
return;
|
|
15953
16026
|
}
|
|
15954
|
-
|
|
15955
|
-
|
|
15956
|
-
|
|
15957
|
-
|
|
15958
|
-
|
|
15959
|
-
|
|
16027
|
+
stopWatching();
|
|
16028
|
+
onSettled();
|
|
16029
|
+
};
|
|
16030
|
+
el.addEventListener("transitionend", onTransitionEnd);
|
|
16031
|
+
const durationsInSeconds = getComputedStyle(el)
|
|
16032
|
+
.transitionDuration.split(",")
|
|
16033
|
+
.map((value) => parseFloat(value) || 0);
|
|
16034
|
+
const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
|
|
16035
|
+
const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
|
|
16036
|
+
return () => {
|
|
16037
|
+
if (settled) {
|
|
16038
|
+
return;
|
|
15960
16039
|
}
|
|
15961
|
-
|
|
15962
|
-
}
|
|
15963
|
-
return vnodes;
|
|
16040
|
+
stopWatching();
|
|
16041
|
+
};
|
|
15964
16042
|
};
|
|
15965
16043
|
|
|
15966
|
-
installImportMetaCssBuild(import.meta);
|
|
15967
16044
|
/**
|
|
15968
|
-
*
|
|
15969
|
-
*
|
|
15970
|
-
*
|
|
15971
|
-
*
|
|
15972
|
-
*
|
|
16045
|
+
* Drops the document's text selection when it lives inside `el`, leaving a
|
|
16046
|
+
* selection made elsewhere on the page alone.
|
|
16047
|
+
*
|
|
16048
|
+
* A popup being closed takes its content with it, and a selection is a claim
|
|
16049
|
+
* on content the user can still act on: kept, it would outlive the surface it
|
|
16050
|
+
* was made on — painted on the box for the length of its exit transition, with
|
|
16051
|
+
* the handles and the copy toolbar a phone draws from the live selection
|
|
16052
|
+
* hanging over something that is going away — and reappear with the box on
|
|
16053
|
+
* the next opening. Dropping the selection is the only way to remove that
|
|
16054
|
+
* chrome: a `user-select: none` on the closing box hides the highlight in some
|
|
16055
|
+
* browsers only, and the handles are the browser's own, drawn from the
|
|
16056
|
+
* selection object rather than from any style.
|
|
15973
16057
|
*/
|
|
15974
|
-
|
|
15975
|
-
const
|
|
15976
|
-
|
|
15977
|
-
|
|
15978
|
-
aspect-ratio: 1;
|
|
15979
|
-
color: #fff;
|
|
15980
|
-
vertical-align: middle;
|
|
15981
|
-
background-color: var(--x-callout-status-icon-color);
|
|
15982
|
-
border-radius: 2px;
|
|
15983
|
-
flex-shrink: 0;
|
|
15984
|
-
justify-content: center;
|
|
15985
|
-
align-items: center;
|
|
15986
|
-
width: 1em;
|
|
15987
|
-
height: 1em;
|
|
15988
|
-
display: inline-flex;
|
|
15989
|
-
|
|
15990
|
-
&[data-status="success"] {
|
|
15991
|
-
--x-callout-status-icon-color: var(--navi-callout-success-color);
|
|
15992
|
-
}
|
|
15993
|
-
|
|
15994
|
-
&[data-status="info"] {
|
|
15995
|
-
--x-callout-status-icon-color: var(--navi-callout-info-color);
|
|
16058
|
+
const clearTextSelectionInside = (el) => {
|
|
16059
|
+
const selection = window.getSelection();
|
|
16060
|
+
if (!selection || selection.rangeCount === 0) {
|
|
16061
|
+
return;
|
|
15996
16062
|
}
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
|
|
16063
|
+
const range = selection.getRangeAt(0);
|
|
16064
|
+
if (!el.contains(range.commonAncestorContainer)) {
|
|
16065
|
+
return;
|
|
16000
16066
|
}
|
|
16067
|
+
selection.removeAllRanges();
|
|
16068
|
+
};
|
|
16001
16069
|
|
|
16002
|
-
|
|
16003
|
-
|
|
16004
|
-
|
|
16070
|
+
/**
|
|
16071
|
+
* Disables pointer-events on `el` until its current CSS transition settles —
|
|
16072
|
+
* avoids the cursor changing/something becoming clickable while the popup is
|
|
16073
|
+
* still visually moving into or out of place.
|
|
16074
|
+
*
|
|
16075
|
+
* Returns whenTransitionSettles' own "cancel" function: it doesn't restore
|
|
16076
|
+
* pointer-events, since a fresh call for the next open/close is about to set
|
|
16077
|
+
* its own state.
|
|
16078
|
+
*/
|
|
16079
|
+
const suppressPointerEventsDuringTransition = (el) => {
|
|
16080
|
+
el.style.pointerEvents = "none";
|
|
16081
|
+
return whenTransitionSettles(el, () => {
|
|
16082
|
+
el.style.pointerEvents = "";
|
|
16083
|
+
});
|
|
16084
|
+
};
|
|
16005
16085
|
|
|
16006
|
-
|
|
16007
|
-
|
|
16086
|
+
/**
|
|
16087
|
+
* Hides the backdrop, deferring until the browser's matching "click" fires
|
|
16088
|
+
* when `closeEvent` was triggered by a mousedown (see popover.jsx's top
|
|
16089
|
+
* comment for why) — same capture-phase-on-document pattern as
|
|
16090
|
+
* armSuppressNextOpenRequest in open_controller.js, which a plain timeout
|
|
16091
|
+
* can't safely replace: mouseup (and the click that follows it) can land an
|
|
16092
|
+
* arbitrarily long time after mousedown (the user is still holding the
|
|
16093
|
+
* button down), so a short timeout can fire first and hide the backdrop
|
|
16094
|
+
* before its own click ever arrives. A capture-phase listener on document
|
|
16095
|
+
* fires for every click regardless of what any bubble-phase handler does
|
|
16096
|
+
* downstream, so no fallback timer is needed.
|
|
16097
|
+
*
|
|
16098
|
+
* `hide` is the caller's own way to actually hide the backdrop
|
|
16099
|
+
* (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
|
|
16100
|
+
* for a plain div) — this helper only owns the mousedown/click timing.
|
|
16101
|
+
*
|
|
16102
|
+
* Returns a disarm function (or undefined if hidden immediately), so a
|
|
16103
|
+
* fresh open can cancel a pending hide it's about to make redundant.
|
|
16104
|
+
*/
|
|
16105
|
+
const armPointerDownOutsideClose = (closeEvent, hide) => {
|
|
16106
|
+
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
16107
|
+
if (!mousedownEvent) {
|
|
16108
|
+
hide();
|
|
16109
|
+
return undefined;
|
|
16008
16110
|
}
|
|
16111
|
+
// `hide` is handed the click, for a wall with peers to re-aim it (see
|
|
16112
|
+
// handWallClickToPeer).
|
|
16113
|
+
const onClick = (clickEvent) => {
|
|
16114
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
16115
|
+
hide(clickEvent);
|
|
16116
|
+
};
|
|
16117
|
+
document.addEventListener("click", onClick, { capture: true });
|
|
16118
|
+
return () => {
|
|
16119
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
16120
|
+
};
|
|
16121
|
+
};
|
|
16009
16122
|
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16123
|
+
/**
|
|
16124
|
+
* Maps a positionArea y/x pair to a concrete `navi-animation` value (a
|
|
16125
|
+
* `prefix` plus a direction word), or `null` if both axes overlap the anchor
|
|
16126
|
+
* (no direction at all — that's `resolvedAnimationKind === "scaling"`
|
|
16127
|
+
* territory instead, see resolveAutoAnimationKind below).
|
|
16128
|
+
*
|
|
16129
|
+
* `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
|
|
16130
|
+
* when docked) keeps the word as the compass direction the popup comes
|
|
16131
|
+
* from: placed "top" (a point/corner), it slides in from the top.
|
|
16132
|
+
* `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
|
|
16133
|
+
* direction instead, the opposite compass point: placed "top" of the
|
|
16134
|
+
* anchor, it moves/grows up, away from the anchor (which sits below it).
|
|
16135
|
+
*
|
|
16136
|
+
* "inset-*"/"center" contribute no direction on their axis either way.
|
|
16137
|
+
*/
|
|
16138
|
+
const resolveDirectionValue = (y, x, { prefix }) => {
|
|
16139
|
+
const yWord =
|
|
16140
|
+
y === "top"
|
|
16141
|
+
? prefix === "expand"
|
|
16142
|
+
? "up"
|
|
16143
|
+
: "top"
|
|
16144
|
+
: y === "bottom"
|
|
16145
|
+
? prefix === "expand"
|
|
16146
|
+
? "down"
|
|
16147
|
+
: "bottom"
|
|
16148
|
+
: null;
|
|
16149
|
+
const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
|
|
16150
|
+
if (!yWord && !xWord) {
|
|
16151
|
+
return null;
|
|
16013
16152
|
}
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
height: 100%;
|
|
16019
|
-
}
|
|
16020
|
-
`;
|
|
16153
|
+
return yWord && xWord
|
|
16154
|
+
? `${prefix}-${yWord}-${xWord}`
|
|
16155
|
+
: `${prefix}-${yWord || xWord}`;
|
|
16156
|
+
};
|
|
16021
16157
|
|
|
16022
16158
|
/**
|
|
16023
|
-
*
|
|
16024
|
-
*
|
|
16025
|
-
*
|
|
16026
|
-
*
|
|
16027
|
-
*
|
|
16159
|
+
* Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
|
|
16160
|
+
* — picked for any real anchor, or for a point/corner placed dead-center
|
|
16161
|
+
* (both positionArea axes overlapping — there's no sensible direction to
|
|
16162
|
+
* slide from in that case). "sliding" otherwise. `anchor` is `undefined`
|
|
16163
|
+
* for any no-anchor/docked case (Dialog always, Popover's own custom
|
|
16164
|
+
* renderer when there's no real anchor), so this collapses to "scaling"
|
|
16165
|
+
* there only for the dead-center case, "sliding" otherwise. The two
|
|
16166
|
+
* "overlapping" booleans below describe the *positionArea* itself (a bare
|
|
16167
|
+
* word vs. "inset-"/"center"), not anything about the anchor — they'd
|
|
16168
|
+
* mean exactly the same thing even with no anchor at all, since it's the
|
|
16169
|
+
* position strategy, not the anchor, that decides whether there's a
|
|
16170
|
+
* direction to slide from.
|
|
16028
16171
|
*/
|
|
16029
|
-
const
|
|
16030
|
-
const
|
|
16172
|
+
const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
|
|
16173
|
+
const yIsOverlapping =
|
|
16174
|
+
parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
|
|
16175
|
+
const xIsOverlapping =
|
|
16176
|
+
parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
|
|
16177
|
+
return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
|
|
16178
|
+
};
|
|
16031
16179
|
|
|
16032
16180
|
/**
|
|
16033
|
-
*
|
|
16034
|
-
*
|
|
16035
|
-
*
|
|
16036
|
-
*
|
|
16037
|
-
*
|
|
16038
|
-
*
|
|
16039
|
-
*
|
|
16040
|
-
*
|
|
16181
|
+
* An opening that reaches a popup with no element of its own to show.
|
|
16182
|
+
*
|
|
16183
|
+
* The one cause seen so far, and it is silent: something inside the popup
|
|
16184
|
+
* suspended while its content was being built, and the `<Loading>` that caught
|
|
16185
|
+
* it sits ABOVE the popup. A boundary suspending on an update keeps the page as
|
|
16186
|
+
* it was, as a copy, so what is opened here belongs to a copy nobody owns any
|
|
16187
|
+
* more — and once the data arrives the subtree is rebuilt from scratch, closed
|
|
16188
|
+
* and empty. Nothing is thrown, nothing is drawn, and the press is lost.
|
|
16041
16189
|
*/
|
|
16042
|
-
const
|
|
16043
|
-
|
|
16044
|
-
|
|
16045
|
-
|
|
16046
|
-
import.meta.css = [css$15, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
|
|
16047
|
-
return jsx("span", {
|
|
16048
|
-
className: "navi_callout_status_icon",
|
|
16049
|
-
"data-status": status === "none" ? undefined : status,
|
|
16050
|
-
"data-shape": shape === "circle" ? "circle" : undefined,
|
|
16051
|
-
"aria-hidden": "true",
|
|
16052
|
-
children: jsx("svg", {
|
|
16053
|
-
viewBox: CALLOUT_STATUS_GLYPH_VIEWBOX,
|
|
16054
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
16055
|
-
children: jsx("path", {
|
|
16056
|
-
fill: "currentColor",
|
|
16057
|
-
d: CALLOUT_STATUS_GLYPH_PATH
|
|
16058
|
-
})
|
|
16059
|
-
})
|
|
16060
|
-
});
|
|
16190
|
+
const warnPopupHasNoElementToOpen = (popupKind) => {
|
|
16191
|
+
console.warn(
|
|
16192
|
+
`[navi] a "${popupKind}" was asked to open and has no element to open. What usually did it: content inside it suspended, and the <Loading> that caught the wait is above the popup rather than inside it — the popup was set aside with the rest of what that boundary holds, and this opening is lost (the next one works). Put a <Loading> inside the popup, or draw the wait in the component itself with useAsyncData(action, { loading: true }).`,
|
|
16193
|
+
);
|
|
16061
16194
|
};
|
|
16062
16195
|
|
|
16063
|
-
|
|
16196
|
+
/**
|
|
16197
|
+
* A popup reads "outside" from its own border box: what a press lands on
|
|
16198
|
+
* decides nothing on its own (a genuine backdrop press and a press on the
|
|
16199
|
+
* popup's padding both report the popup element as their target, there being
|
|
16200
|
+
* no real ::backdrop node to be one), so the rectangle is what tells them
|
|
16201
|
+
* apart. That holds as long as the box and what the popup paints are the same
|
|
16202
|
+
* thing — which stops being true for a popup with no surface of its own
|
|
16203
|
+
* (`backgroundColor="transparent"`, no shadow, no padding): what the eye reads
|
|
16204
|
+
* as backdrop is then inside the box, and a press there stays a press on the
|
|
16205
|
+
* popup.
|
|
16206
|
+
*
|
|
16207
|
+
* `data-navi-popup-outside` is how a caller says which of its own boxes are
|
|
16208
|
+
* not the surface. It is opt-in because navi cannot infer it — a background
|
|
16209
|
+
* can come from anywhere — while the caller who made the popup see-through
|
|
16210
|
+
* knows exactly which box is decoration and which is paper.
|
|
16211
|
+
*
|
|
16212
|
+
* The marker answers for the element it is on, never for its descendants: a
|
|
16213
|
+
* box painted inside a marked one is still surface, so a row can be marked and
|
|
16214
|
+
* its empty halves read as backdrop while presses on the controls it holds do
|
|
16215
|
+
* not dismiss anything. A descendant with `pointer-events: none` never becomes
|
|
16216
|
+
* a press target at all, so the marked box answers in its place — while the
|
|
16217
|
+
* same declaration on the marked box itself takes it out of hit-testing and
|
|
16218
|
+
* makes the marker unreachable (see warnAboutUnreachableOutsideRegions).
|
|
16219
|
+
*/
|
|
16220
|
+
const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
|
|
16064
16221
|
|
|
16065
|
-
|
|
16066
|
-
|
|
16222
|
+
/**
|
|
16223
|
+
* `data-navi-popup-inside` is the other direction: a box of the PAGE the
|
|
16224
|
+
* caller says belongs to a popup, so a press on it is not outside that popup
|
|
16225
|
+
* even though it lies beyond the border box — a card whose press is what
|
|
16226
|
+
* fills the one panel of a board. Its value names the popup by `id`, the way
|
|
16227
|
+
* `commandfor` does (several ids, space separated), because the exemption is
|
|
16228
|
+
* one popup's: a menu open beside the board still closes on the same press.
|
|
16229
|
+
*
|
|
16230
|
+
* Unlike the outside marker it answers for its whole subtree: the outside
|
|
16231
|
+
* marker carves free space out of a surface whose controls stay surface, this
|
|
16232
|
+
* one adds a whole thing — the card, its count, its buttons — to the popup's
|
|
16233
|
+
* own ground, and nothing in it is a dismissal.
|
|
16234
|
+
*/
|
|
16235
|
+
const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
|
|
16067
16236
|
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
16071
|
-
|
|
16237
|
+
/**
|
|
16238
|
+
* `data-navi-popup-group` names the peers of a popup: the triggers of the other
|
|
16239
|
+
* popups a press goes to from this one — a menu bar, where pressing another
|
|
16240
|
+
* menu while one is open switches to it, in that one press. A trigger joins by
|
|
16241
|
+
* carrying the name (a Picker's `popupGroup` writes it on its own), and a popup
|
|
16242
|
+
* reads it off what the press landed on.
|
|
16243
|
+
*
|
|
16244
|
+
* Returns the peer `target` is in, or null.
|
|
16245
|
+
*/
|
|
16246
|
+
const findPopupPeer = (target, group) => {
|
|
16247
|
+
if (!group || !target || typeof target.closest !== "function") {
|
|
16248
|
+
return null;
|
|
16249
|
+
}
|
|
16250
|
+
return target.closest(`[data-navi-popup-group="${CSS.escape(group)}"]`);
|
|
16251
|
+
};
|
|
16252
|
+
|
|
16253
|
+
/**
|
|
16254
|
+
* The click a wall took, handed to the peer (see findPopupPeer) it covered.
|
|
16255
|
+
*
|
|
16256
|
+
* A wall cannot let one press through and not another: it is what the pointer
|
|
16257
|
+
* hits, and that is the whole of how it spends a press. So it goes on taking
|
|
16258
|
+
* every one — the page behind stays as unhoverable and unpressable as it looks
|
|
16259
|
+
* — and once the press is over, its click is re-aimed at what the wall stood
|
|
16260
|
+
* over at that point, when that is a peer. The click alone, for the reason
|
|
16261
|
+
* transition_press.js gives: a pointer stream made up with no pointer behind it
|
|
16262
|
+
* starts gestures (a hold, a drag) that nothing ever ends, and a click is what
|
|
16263
|
+
* opens a popup's trigger.
|
|
16264
|
+
*
|
|
16265
|
+
* Called with the click that takes the wall down after a press closed the
|
|
16266
|
+
* popup (see armPointerDownOutsideClose), so the peer opens as the popup it
|
|
16267
|
+
* replaces goes.
|
|
16268
|
+
*/
|
|
16269
|
+
const handWallClickToPeer = (
|
|
16270
|
+
clickEvent,
|
|
16271
|
+
{ wallEl, popupEl, anchorEl, group },
|
|
16272
|
+
) => {
|
|
16273
|
+
if (!clickEvent.isTrusted || clickEvent.target !== wallEl) {
|
|
16274
|
+
// A click dispatched by hand is already aimed, and one that did not land
|
|
16275
|
+
// on the wall (a press on the popup's own outside region) was no wall's.
|
|
16276
|
+
return;
|
|
16277
|
+
}
|
|
16278
|
+
// The topmost element under the wall at that point: what the press would
|
|
16279
|
+
// have landed on with no wall. Asked past the wall rather than after hiding
|
|
16280
|
+
// it, which a fading wall may not be yet.
|
|
16281
|
+
const covered = document
|
|
16282
|
+
.elementsFromPoint(clickEvent.clientX, clickEvent.clientY)
|
|
16283
|
+
.find((el) => el !== wallEl && !popupEl.contains(el));
|
|
16284
|
+
const peer = findPopupPeer(covered, group);
|
|
16285
|
+
if (!peer) {
|
|
16286
|
+
return;
|
|
16287
|
+
}
|
|
16288
|
+
if (peer.contains(popupEl) || (anchorEl && peer.contains(anchorEl))) {
|
|
16289
|
+
// The popup's own trigger — the one holding it (a picker renders its
|
|
16290
|
+
// popup inside itself) or the one it opened against: the press through
|
|
16291
|
+
// the wall closed the popup, and handing the trigger its click would open
|
|
16292
|
+
// it again.
|
|
16293
|
+
return;
|
|
16294
|
+
}
|
|
16295
|
+
// The page sees one click, on the peer, rather than two.
|
|
16296
|
+
clickEvent.stopPropagation();
|
|
16297
|
+
// A PointerEvent where the browser makes clicks of them, a MouseEvent
|
|
16298
|
+
// elsewhere: the same kind, copied field by field.
|
|
16299
|
+
const ClickEvent = clickEvent.constructor;
|
|
16300
|
+
covered.dispatchEvent(new ClickEvent(clickEvent.type, clickEvent));
|
|
16301
|
+
};
|
|
16302
|
+
|
|
16303
|
+
/**
|
|
16304
|
+
* What a press landing on a region the caller declared as not-its-surface
|
|
16305
|
+
* does: exactly what the same press on the backdrop would do.
|
|
16306
|
+
*
|
|
16307
|
+
* Lives on the popup's own content element rather than in the backdrop's
|
|
16308
|
+
* handler, for both renderers: the backdrop is a sibling behind the popup
|
|
16309
|
+
* (never an ancestor), so a press inside the popup's box never reaches it.
|
|
16310
|
+
*/
|
|
16311
|
+
const handlePressOnOutsideRegion = (
|
|
16312
|
+
mouseDownEvent,
|
|
16313
|
+
{ popupEl, openController, pressOutside },
|
|
16314
|
+
) => {
|
|
16315
|
+
if (mouseDownEvent.button !== 0) {
|
|
16316
|
+
return;
|
|
16317
|
+
}
|
|
16318
|
+
const { target } = mouseDownEvent;
|
|
16319
|
+
if (!target.hasAttribute(OUTSIDE_REGION_ATTRIBUTE)) {
|
|
16320
|
+
return;
|
|
16321
|
+
}
|
|
16322
|
+
if (openedDuringThisPress(openController)) {
|
|
16323
|
+
// The release of the press that opened it is not somebody dismissing it
|
|
16324
|
+
// (see openedDuringThisPress).
|
|
16325
|
+
return;
|
|
16326
|
+
}
|
|
16327
|
+
// A popup opens inside its opener's own subtree, so a press on a region
|
|
16328
|
+
// belonging to a popup nested in this one bubbles through here too — and it
|
|
16329
|
+
// is a press on what is in front, not on this popup's own decoration.
|
|
16330
|
+
if (
|
|
16331
|
+
target.closest(`[navi-control="dialog"], [navi-control="popover"]`) !==
|
|
16332
|
+
popupEl
|
|
16333
|
+
) {
|
|
16334
|
+
return;
|
|
16335
|
+
}
|
|
16336
|
+
if (pressOutside === "capture") {
|
|
16337
|
+
mouseDownEvent.preventDefault();
|
|
16338
|
+
return;
|
|
16339
|
+
}
|
|
16340
|
+
if (pressOutside === "close" || pressOutside === "cancel") {
|
|
16341
|
+
openController.requestClose(mouseDownEvent, {
|
|
16342
|
+
isCancel: pressOutside === "cancel",
|
|
16343
|
+
});
|
|
16344
|
+
}
|
|
16345
|
+
};
|
|
16346
|
+
|
|
16347
|
+
/**
|
|
16348
|
+
* `pointer-events: none` and `inert` are the two things reached for to say
|
|
16349
|
+
* "this part of my box is not really there", and neither reaches the popup:
|
|
16350
|
+
* inert speaks to the keyboard and to assistive technology, and
|
|
16351
|
+
* pointer-events takes the box out of hit-testing so the press is answered by
|
|
16352
|
+
* an ancestor that is still the popup's own surface. Writing them next to the
|
|
16353
|
+
* marker is the natural mistake, and it makes the marker silently unreachable
|
|
16354
|
+
* — so it is said out loud, once per open.
|
|
16355
|
+
*/
|
|
16356
|
+
const warnAboutUnreachableOutsideRegions = (popupEl) => {
|
|
16357
|
+
for (const regionEl of popupEl.querySelectorAll(
|
|
16358
|
+
`[${OUTSIDE_REGION_ATTRIBUTE}]`,
|
|
16359
|
+
)) {
|
|
16360
|
+
if (getComputedStyle(regionEl).pointerEvents === "none") {
|
|
16361
|
+
console.warn(
|
|
16362
|
+
`[navi] ${OUTSIDE_REGION_ATTRIBUTE} on an element with "pointer-events: none" is never read: the press is answered by the nearest ancestor still in hit-testing, which is the popup itself. Drop pointer-events; the marker alone is what makes a press there count as outside.`,
|
|
16363
|
+
regionEl,
|
|
16364
|
+
);
|
|
16365
|
+
}
|
|
16366
|
+
}
|
|
16367
|
+
};
|
|
16368
|
+
|
|
16369
|
+
/**
|
|
16370
|
+
* Hears an outside press from the document itself, for a popup with no
|
|
16371
|
+
* backdrop of its own to catch one: a modal `<dialog>` (whose native
|
|
16372
|
+
* `::backdrop` dispatches nothing a handler can be put on) and any popup the
|
|
16373
|
+
* caller asked to leave the page reachable (`backdrop={false}`).
|
|
16374
|
+
*
|
|
16375
|
+
* The press is read and never taken: no `preventDefault`, no
|
|
16376
|
+
* `stopPropagation`, and the listener is on `document` rather than on
|
|
16377
|
+
* anything covering the page — so what the press landed on hears it too, in
|
|
16378
|
+
* the same gesture, and the popup only takes note. That is the whole point of
|
|
16379
|
+
* a popup with no wall: a press outside is a press on the page, and the page
|
|
16380
|
+
* answers it. The one popup where that cannot hold is the modal dialog, where
|
|
16381
|
+
* the browser has already made everything behind inert before this runs.
|
|
16382
|
+
*
|
|
16383
|
+
* `pressEventType` says which event IS the press, and the two callers want
|
|
16384
|
+
* opposite ones:
|
|
16385
|
+
*
|
|
16386
|
+
* - `"pointerdown"` for a popup with no wall: a page with no wall over it is
|
|
16387
|
+
* a page whose own elements arbitrate their presses, and cancelling a
|
|
16388
|
+
* `pointerdown` — what a drag source and a control keeping the focus where
|
|
16389
|
+
* it is both do — suppresses every compatibility mouse event that would have
|
|
16390
|
+
* followed it. The press happens, reaches its target, and no `mousedown` is
|
|
16391
|
+
* ever dispatched for it. Reading the pointer event is reading the press
|
|
16392
|
+
* itself, whatever anything does with it afterwards, and it is the same
|
|
16393
|
+
* event the drag and surface detectors read.
|
|
16394
|
+
* - `"mousedown"` for the modal dialog, whose native `::backdrop` exists to
|
|
16395
|
+
* spend the press. The page's controls act on `mousedown`, and on a touch
|
|
16396
|
+
* screen that is not the same moment as `pointerdown`: the compatibility
|
|
16397
|
+
* mouse events are synthesized at `touchend` and hit-tested again then.
|
|
16398
|
+
* Closing on `pointerdown` takes the `::backdrop` away before that second
|
|
16399
|
+
* hit-test, so the `mousedown` lands on whatever the page holds under the
|
|
16400
|
+
* finger, and one tap both dismisses the dialog and presses the page. Read
|
|
16401
|
+
* on `mousedown`, the wall is still up when the press the page acts on is
|
|
16402
|
+
* aimed, and it catches it. Nothing behind a modal wall can cancel that
|
|
16403
|
+
* press, since nothing behind it hears the `pointerdown` at all.
|
|
16404
|
+
*
|
|
16405
|
+
* Capture phase, so a handler downstream that stops propagation cannot keep
|
|
16406
|
+
* the popup open either — the press is still outside whatever is made of it.
|
|
16407
|
+
*
|
|
16408
|
+
* Returns a cleanup that removes the listener.
|
|
16409
|
+
*/
|
|
16410
|
+
const armOutsidePressClose = (
|
|
16411
|
+
popupEl,
|
|
16412
|
+
{ openController, pressOutside, pressEventType = "pointerdown" },
|
|
16413
|
+
) => {
|
|
16414
|
+
const onDocumentPointerDown = (pointerDownEvent) => {
|
|
16415
|
+
if (pointerDownEvent.button !== 0) {
|
|
16416
|
+
return;
|
|
16417
|
+
}
|
|
16418
|
+
if (openedDuringThisPress(openController)) {
|
|
16419
|
+
// The release of the press that opened it is not somebody dismissing it
|
|
16420
|
+
// (see openedDuringThisPress).
|
|
16421
|
+
return;
|
|
16422
|
+
}
|
|
16423
|
+
// The press landed inside another popup: that is a press on what is in
|
|
16424
|
+
// front, not outside. Asking the target where it lives rather than asking
|
|
16425
|
+
// this popup whether it was pushed — a popup in front does not have to be
|
|
16426
|
+
// one this popup knows about. In front means neither inside nor around: a
|
|
16427
|
+
// popup nested in this one falls to the containment check below as the
|
|
16428
|
+
// inside press it is, and one this popup opened INSIDE (a bubble rendered
|
|
16429
|
+
// in a sheet's content) is around it, so what that popup holds is outside
|
|
16430
|
+
// this one exactly like the rest of the page.
|
|
16431
|
+
const popupUnderPointer = pointerDownEvent.target.closest?.(
|
|
16432
|
+
`[navi-control="dialog"], [navi-control="popover"]`,
|
|
16433
|
+
);
|
|
16434
|
+
if (
|
|
16435
|
+
popupUnderPointer &&
|
|
16436
|
+
popupUnderPointer !== popupEl &&
|
|
16437
|
+
!popupEl.contains(popupUnderPointer) &&
|
|
16438
|
+
!popupUnderPointer.contains(popupEl)
|
|
16439
|
+
) {
|
|
16440
|
+
return;
|
|
16441
|
+
}
|
|
16442
|
+
if (
|
|
16443
|
+
popupEl.id &&
|
|
16444
|
+
pointerDownEvent.target.closest?.(
|
|
16445
|
+
`[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupEl.id)}"]`,
|
|
16446
|
+
)
|
|
16447
|
+
) {
|
|
16448
|
+
return;
|
|
16449
|
+
}
|
|
16450
|
+
// Real DOM containment wins over the rectangle below — an element
|
|
16451
|
+
// genuinely inside the popup (`overflow: visible`, a negative margin, an
|
|
16452
|
+
// absolutely-positioned child) can be painted outside its border box, and
|
|
16453
|
+
// a press there is not outside just because its coordinates are. The
|
|
16454
|
+
// popup element itself is excluded (contains() answers true for it): a
|
|
16455
|
+
// press on a modal dialog's native backdrop reports the dialog as its
|
|
16456
|
+
// target, there being no `::backdrop` node to be one, so treating that as
|
|
16457
|
+
// contained would keep the rectangle from ever running.
|
|
16458
|
+
if (
|
|
16459
|
+
pointerDownEvent.target !== popupEl &&
|
|
16460
|
+
popupEl.contains(pointerDownEvent.target)
|
|
16461
|
+
) {
|
|
16462
|
+
return;
|
|
16463
|
+
}
|
|
16464
|
+
const rect = popupEl.getBoundingClientRect();
|
|
16465
|
+
const isOutside =
|
|
16466
|
+
pointerDownEvent.clientX < rect.left ||
|
|
16467
|
+
pointerDownEvent.clientX > rect.right ||
|
|
16468
|
+
pointerDownEvent.clientY < rect.top ||
|
|
16469
|
+
pointerDownEvent.clientY > rect.bottom;
|
|
16470
|
+
if (!isOutside) {
|
|
16471
|
+
return;
|
|
16472
|
+
}
|
|
16473
|
+
openController.requestClose(pointerDownEvent, {
|
|
16474
|
+
isCancel: pressOutside === "cancel",
|
|
16475
|
+
});
|
|
16476
|
+
};
|
|
16477
|
+
document.addEventListener(pressEventType, onDocumentPointerDown, {
|
|
16478
|
+
capture: true,
|
|
16479
|
+
});
|
|
16480
|
+
return () => {
|
|
16481
|
+
document.removeEventListener(pressEventType, onDocumentPointerDown, {
|
|
16482
|
+
capture: true,
|
|
16483
|
+
});
|
|
16484
|
+
};
|
|
16485
|
+
};
|
|
16486
|
+
|
|
16487
|
+
/**
|
|
16488
|
+
* Typography reset shared by the surfaces a control opens: callout, popover,
|
|
16489
|
+
* dialog.
|
|
16490
|
+
*
|
|
16491
|
+
* A surface is painted on top of the page but lives in the DOM subtree of the
|
|
16492
|
+
* element it opens from, so every inherited text property of that element
|
|
16493
|
+
* reaches it. The ink an element chose for its own background — centered,
|
|
16494
|
+
* shadowed, uppercase, letter-spaced, kept on one line — arrives on paper that
|
|
16495
|
+
* has none of that background, and the caller discovers it as a symptom
|
|
16496
|
+
* (a blurred tooltip, a centered message) with nothing on screen pointing back
|
|
16497
|
+
* to the rule three elements up. A surface writes its own text, on its own
|
|
16498
|
+
* paper, in the document's terms.
|
|
16499
|
+
*
|
|
16500
|
+
* Every one of them is set to its initial value rather than `revert`: only
|
|
16501
|
+
* `color` and `background-color` are declared by the UA on a `[popover]`/
|
|
16502
|
+
* `<dialog>`, so `revert` on any of these would roll back to the inherited
|
|
16503
|
+
* value — the very thing to stop. `text-align: initial` is `start`, which
|
|
16504
|
+
* still reads the surface's own `direction`, so an RTL document keeps its
|
|
16505
|
+
* side.
|
|
16506
|
+
*
|
|
16507
|
+
* `font-family` is deliberately absent: a surface keeps the face of what
|
|
16508
|
+
* opened it, so a section written in a display font gets its tooltips in that
|
|
16509
|
+
* font. `color`, `font-size` and `font-weight` are absent too — each surface
|
|
16510
|
+
* answers those its own way (a popup writes in --navi-popup-color and follows
|
|
16511
|
+
* the size of what opened it; a callout reverts to the document's ink and
|
|
16512
|
+
* size).
|
|
16513
|
+
*
|
|
16514
|
+
* In `@layer navi` so an app that wants one of these back says so on the
|
|
16515
|
+
* surface and wins, without having to out-specify anything.
|
|
16516
|
+
*/
|
|
16517
|
+
const surfaceTextCss = /* css */ `
|
|
16518
|
+
@layer navi {
|
|
16519
|
+
.navi_callout,
|
|
16520
|
+
.navi_popover,
|
|
16521
|
+
.navi_dialog {
|
|
16522
|
+
font-style: initial;
|
|
16523
|
+
text-align: initial;
|
|
16524
|
+
text-indent: initial;
|
|
16525
|
+
text-transform: initial;
|
|
16526
|
+
text-shadow: none;
|
|
16527
|
+
white-space: initial;
|
|
16528
|
+
word-spacing: initial;
|
|
16529
|
+
letter-spacing: initial;
|
|
16530
|
+
}
|
|
16531
|
+
}
|
|
16532
|
+
`;
|
|
16533
|
+
|
|
16534
|
+
const CalloutContext = createContext();
|
|
16535
|
+
const useCalloutRequestClose = () => {
|
|
16536
|
+
return useContext(CalloutContext)?.requestClose;
|
|
16537
|
+
};
|
|
16538
|
+
const useCalloutElement = () => {
|
|
16539
|
+
return useContext(CalloutContext)?.element;
|
|
16540
|
+
};
|
|
16541
|
+
const renderIntoCallout = (jsx$1, calloutMessageElement, {
|
|
16542
|
+
requestClose,
|
|
16543
|
+
element
|
|
16544
|
+
}) => {
|
|
16545
|
+
const calloutJsx = jsx(CalloutContext.Provider, {
|
|
16546
|
+
value: {
|
|
16547
|
+
requestClose,
|
|
16548
|
+
element
|
|
16549
|
+
},
|
|
16550
|
+
children: jsx$1
|
|
16551
|
+
});
|
|
16552
|
+
render(calloutJsx, calloutMessageElement);
|
|
16553
|
+
};
|
|
16554
|
+
|
|
16555
|
+
// An HTML message is rendered through preact rather than innerHTML: what a
|
|
16556
|
+
// message holds is then a preact tree like any other, so a callout's own JSX
|
|
16557
|
+
// (its context, its close button) works the same whether the message arrived
|
|
16558
|
+
// as markup or as elements.
|
|
16559
|
+
const renderHtmlIntoCallout = (html, calloutMessageElement, {
|
|
16560
|
+
requestClose,
|
|
16561
|
+
element
|
|
16562
|
+
}) => {
|
|
16563
|
+
const template = document.createElement("template");
|
|
16564
|
+
template.innerHTML = html;
|
|
16565
|
+
renderIntoCallout(domToVNodes(template.content), calloutMessageElement, {
|
|
16566
|
+
requestClose,
|
|
16567
|
+
element
|
|
16568
|
+
});
|
|
16569
|
+
};
|
|
16570
|
+
|
|
16571
|
+
// Unmounts whatever preact rendered before the element is filled by hand
|
|
16572
|
+
// (a DOM node, an iframe); innerHTML alone would leave preact believing its
|
|
16573
|
+
// tree is still there.
|
|
16574
|
+
const clearCalloutMessage = calloutMessageElement => {
|
|
16575
|
+
render(null, calloutMessageElement);
|
|
16576
|
+
calloutMessageElement.innerHTML = "";
|
|
16577
|
+
};
|
|
16578
|
+
const domToVNodes = node => {
|
|
16579
|
+
const vnodes = [];
|
|
16580
|
+
for (const child of node.childNodes) {
|
|
16581
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
16582
|
+
vnodes.push(child.data);
|
|
16583
|
+
continue;
|
|
16584
|
+
}
|
|
16585
|
+
if (child.nodeType !== Node.ELEMENT_NODE) {
|
|
16586
|
+
continue;
|
|
16587
|
+
}
|
|
16588
|
+
const props = {};
|
|
16589
|
+
for (const {
|
|
16590
|
+
name,
|
|
16591
|
+
value
|
|
16592
|
+
} of child.attributes) {
|
|
16593
|
+
props[name] = value;
|
|
16594
|
+
}
|
|
16595
|
+
vnodes.push(h(child.localName, props, ...domToVNodes(child)));
|
|
16596
|
+
}
|
|
16597
|
+
return vnodes;
|
|
16598
|
+
};
|
|
16599
|
+
|
|
16600
|
+
installImportMetaCssBuild(import.meta);
|
|
16601
|
+
/**
|
|
16602
|
+
* The icon a callout shows beside its message — a square in the status color
|
|
16603
|
+
* with the "!" glyph — drawable on its own, where the callout is not: on the
|
|
16604
|
+
* trigger that opens one (a picker in callout mode), so what one presses looks
|
|
16605
|
+
* like what it opens. The callout's own template draws from the same glyph
|
|
16606
|
+
* (see calloutTemplate in callout.js).
|
|
16607
|
+
*/
|
|
16608
|
+
|
|
16609
|
+
const css$15 = /* css */`.navi_callout_status_icon {
|
|
16610
|
+
--x-callout-status-icon-color: var(--navi-callout-neutral-color);
|
|
16611
|
+
box-sizing: border-box;
|
|
16612
|
+
aspect-ratio: 1;
|
|
16613
|
+
color: #fff;
|
|
16614
|
+
vertical-align: middle;
|
|
16615
|
+
background-color: var(--x-callout-status-icon-color);
|
|
16616
|
+
border-radius: 2px;
|
|
16617
|
+
flex-shrink: 0;
|
|
16618
|
+
justify-content: center;
|
|
16619
|
+
align-items: center;
|
|
16620
|
+
width: 1em;
|
|
16621
|
+
height: 1em;
|
|
16622
|
+
display: inline-flex;
|
|
16623
|
+
|
|
16624
|
+
&[data-status="success"] {
|
|
16625
|
+
--x-callout-status-icon-color: var(--navi-callout-success-color);
|
|
16626
|
+
}
|
|
16627
|
+
|
|
16628
|
+
&[data-status="info"] {
|
|
16629
|
+
--x-callout-status-icon-color: var(--navi-callout-info-color);
|
|
16630
|
+
}
|
|
16631
|
+
|
|
16632
|
+
&[data-status="warning"] {
|
|
16633
|
+
--x-callout-status-icon-color: var(--navi-callout-warning-color);
|
|
16634
|
+
}
|
|
16635
|
+
|
|
16636
|
+
&[data-status="error"] {
|
|
16637
|
+
--x-callout-status-icon-color: var(--navi-callout-error-color);
|
|
16638
|
+
}
|
|
16639
|
+
|
|
16640
|
+
&[data-shape="circle"] {
|
|
16641
|
+
border-radius: 50%;
|
|
16642
|
+
}
|
|
16643
|
+
|
|
16644
|
+
& svg {
|
|
16645
|
+
width: 100%;
|
|
16646
|
+
height: 100%;
|
|
16647
|
+
}
|
|
16648
|
+
}
|
|
16649
|
+
|
|
16650
|
+
.navi_icon > .navi_callout_status_icon {
|
|
16651
|
+
width: 100%;
|
|
16652
|
+
height: 100%;
|
|
16653
|
+
}
|
|
16654
|
+
`;
|
|
16655
|
+
|
|
16656
|
+
/**
|
|
16657
|
+
* The "!" on a square, viewBox and path both. The box is square and holds the
|
|
16658
|
+
* glyph's margin: the ink is 298 of 540 units tall (55%), centered. Drawn this
|
|
16659
|
+
* way rather than tight around the ink so whoever shows it can give the svg the
|
|
16660
|
+
* whole square — the glyph is then placed by the vector rasteriser, which has
|
|
16661
|
+
* no device pixel grid to miss, at any square size down to a fractional one.
|
|
16662
|
+
*/
|
|
16663
|
+
const CALLOUT_STATUS_GLYPH_VIEWBOX = "0 0 540 540";
|
|
16664
|
+
const CALLOUT_STATUS_GLYPH_PATH = "m232.5,121 8,196h59l8-196zm36.5,224a37,37 0 1,0 2,0z";
|
|
16665
|
+
|
|
16666
|
+
/**
|
|
16667
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
16668
|
+
* status?: "info" | "warning" | "error" | "success" | "none",
|
|
16669
|
+
* shape?: "square" | "circle",
|
|
16670
|
+
* }>}
|
|
16671
|
+
* @param {"info"|"warning"|"error"|"success"|"none"} [status="info"] The color
|
|
16672
|
+
* — the callout's own for that status. `"none"` is the neutral one.
|
|
16673
|
+
* @param {"square"|"circle"} [shape="square"] Square like the callout's own
|
|
16674
|
+
* icon, or a circle.
|
|
16675
|
+
*/
|
|
16676
|
+
const CalloutStatusIcon = ({
|
|
16677
|
+
status = "info",
|
|
16678
|
+
shape = "square"
|
|
16679
|
+
}) => {
|
|
16680
|
+
import.meta.css = [css$15, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
|
|
16681
|
+
return jsx("span", {
|
|
16682
|
+
className: "navi_callout_status_icon",
|
|
16683
|
+
"data-status": status === "none" ? undefined : status,
|
|
16684
|
+
"data-shape": shape === "circle" ? "circle" : undefined,
|
|
16685
|
+
"aria-hidden": "true",
|
|
16686
|
+
children: jsx("svg", {
|
|
16687
|
+
viewBox: CALLOUT_STATUS_GLYPH_VIEWBOX,
|
|
16688
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
16689
|
+
children: jsx("path", {
|
|
16690
|
+
fill: "currentColor",
|
|
16691
|
+
d: CALLOUT_STATUS_GLYPH_PATH
|
|
16692
|
+
})
|
|
16693
|
+
})
|
|
16694
|
+
});
|
|
16695
|
+
};
|
|
16696
|
+
|
|
16697
|
+
installImportMetaCssBuild(import.meta);
|
|
16698
|
+
|
|
16699
|
+
// Unique for the page's lifetime: a caller may write the id in a commandfor.
|
|
16700
|
+
let calloutCount = 0;
|
|
16701
|
+
|
|
16702
|
+
// How far `closeByScroll: true` lets the page move under the callout before
|
|
16703
|
+
// dismissing it: small enough that any deliberate scroll gesture reaches it,
|
|
16704
|
+
// large enough to survive the pixel or two a trackpad rests on.
|
|
16705
|
+
const CLOSE_BY_SCROLL_DISTANCE = 10;
|
|
16706
|
+
|
|
16707
|
+
// The events of a press a `backdrop` callout spends, up to its click (which
|
|
16708
|
+
// closes the callout and is spent there), and `pointercancel`, which says the
|
|
16709
|
+
// browser took the press. No touch events: they are the scroll's, and what
|
|
16710
|
+
// listens to them — a list learning the user took its scroll over — is
|
|
16711
|
+
// watching the finger, not answering the press.
|
|
16712
|
+
const SPENT_PRESS_EVENT_TYPES = [
|
|
16713
|
+
"pointerdown",
|
|
16714
|
+
"pointerup",
|
|
16715
|
+
"pointercancel",
|
|
16716
|
+
"mousedown",
|
|
16717
|
+
"mouseup",
|
|
16718
|
+
"contextmenu",
|
|
16719
|
+
];
|
|
16720
|
+
// Where a press lands on something drawn over the page rather than on the page
|
|
16721
|
+
// itself: a popup of navi's (either layer) or a native top-layer element.
|
|
16722
|
+
const LAYER_SELECTOR =
|
|
16723
|
+
'[popover], dialog, [navi-control="popover"], [navi-control="dialog"]';
|
|
16072
16724
|
|
|
16073
16725
|
const css$14 = /* css */ `
|
|
16074
16726
|
/* jsenv-css-opaque: same as dialog.jsx, cascade order. */
|
|
@@ -16358,6 +17010,17 @@ const css$14 = /* css */ `
|
|
|
16358
17010
|
* the callout opened, so scrolling back cancels nothing but a round trip shorter than the
|
|
16359
17011
|
* distance leaves it open. Unlike an outside click it also dismisses an "error" callout: the
|
|
16360
17012
|
* caller asked for it explicitly.
|
|
17013
|
+
* @param {boolean} [options.backdrop=false] - Spend an outside press on closing the callout,
|
|
17014
|
+
* the way a popup's wall spends it: no link followed, no control pressed, no focus moved, no
|
|
17015
|
+
* hold or drag started under it. Without it that press closes the callout and is answered by
|
|
17016
|
+
* what it landed on too. Not a wall, though: the scroll a finger starts beside the callout
|
|
17017
|
+
* still scrolls, a press on a layer drawn over the page (a sheet the callout's content
|
|
17018
|
+
* opened) is that layer's, and a press that does not close the callout (an "error" one) is
|
|
17019
|
+
* not spent either
|
|
17020
|
+
* @param {string} [options.group] - With `backdrop`: the name the callout's peers carry in
|
|
17021
|
+
* `data-navi-popup-group` — the triggers of other popups, callouts or not. A press inside a
|
|
17022
|
+
* peer still closes the callout but is not spent, so the peer it landed on opens its own
|
|
17023
|
+
* popup in the same press — a menu bar, where pressing another menu switches to it
|
|
16361
17024
|
* @param {Event} [options.openingEvent] - The gesture the callout belongs to. It names the
|
|
16362
17025
|
* opener — `currentTarget` while the event is still dispatching, `target` once it is over,
|
|
16363
17026
|
* so awaiting before opening changes nothing — and the opener is the one part of the anchor
|
|
@@ -16422,6 +17085,8 @@ const openCallout = (
|
|
|
16422
17085
|
closeByPressOutside = status === "info",
|
|
16423
17086
|
closeOnFocusLeave = closeByPressOutside,
|
|
16424
17087
|
closeByScroll = false,
|
|
17088
|
+
backdrop = false,
|
|
17089
|
+
group,
|
|
16425
17090
|
closeButton = true,
|
|
16426
17091
|
openingEvent,
|
|
16427
17092
|
reopen = "toggle",
|
|
@@ -16507,6 +17172,9 @@ const openCallout = (
|
|
|
16507
17172
|
reason,
|
|
16508
17173
|
});
|
|
16509
17174
|
};
|
|
17175
|
+
// Asked again at each press rather than once: `update` changes both.
|
|
17176
|
+
const pressOutsideCloses = () =>
|
|
17177
|
+
closeByPressOutside && callout.status !== "error";
|
|
16510
17178
|
const onRequestClose = (event) => {
|
|
16511
17179
|
if (!callout.opened) {
|
|
16512
17180
|
return;
|
|
@@ -16515,10 +17183,7 @@ const openCallout = (
|
|
|
16515
17183
|
const clickOrSpaceOutside =
|
|
16516
17184
|
reason === "click_outside" || reason === "space_outside";
|
|
16517
17185
|
if (clickOrSpaceOutside) {
|
|
16518
|
-
if (!
|
|
16519
|
-
return;
|
|
16520
|
-
}
|
|
16521
|
-
if (callout.status === "error") {
|
|
17186
|
+
if (!pressOutsideCloses()) {
|
|
16522
17187
|
return;
|
|
16523
17188
|
}
|
|
16524
17189
|
} else if (reason === "focus_leave") {
|
|
@@ -16793,16 +17458,36 @@ const openCallout = (
|
|
|
16793
17458
|
}
|
|
16794
17459
|
return openerElement === target || openerElement.contains(target);
|
|
16795
17460
|
};
|
|
17461
|
+
// Whether an outside press on `target` is spent on closing the callout
|
|
17462
|
+
// (`backdrop`). What is not the page behind the callout keeps its press: a
|
|
17463
|
+
// layer the callout is not part of — its own page may well be a sheet, a
|
|
17464
|
+
// sheet its content opened is in front of it — and a peer, which the press
|
|
17465
|
+
// is meant to open.
|
|
17466
|
+
const spendsPressOn = (target) => {
|
|
17467
|
+
if (!backdrop || !pressOutsideCloses()) {
|
|
17468
|
+
return false;
|
|
17469
|
+
}
|
|
17470
|
+
const layer = target.closest?.(LAYER_SELECTOR);
|
|
17471
|
+
if (layer && !layer.contains(calloutElement)) {
|
|
17472
|
+
return false;
|
|
17473
|
+
}
|
|
17474
|
+
if (findPopupPeer(target, group)) {
|
|
17475
|
+
return false;
|
|
17476
|
+
}
|
|
17477
|
+
return true;
|
|
17478
|
+
};
|
|
17479
|
+
const spend = (event) => {
|
|
17480
|
+
debug(event, `outside press spent on closing the callout (backdrop)`);
|
|
17481
|
+
event.preventDefault();
|
|
17482
|
+
event.stopPropagation();
|
|
17483
|
+
};
|
|
16796
17484
|
const handleClickOutside = (event) => {
|
|
16797
17485
|
if (event.button !== 0) {
|
|
16798
17486
|
// right click
|
|
16799
17487
|
return;
|
|
16800
17488
|
}
|
|
16801
17489
|
const clickTarget = event.target;
|
|
16802
|
-
if (
|
|
16803
|
-
clickTarget === calloutElement ||
|
|
16804
|
-
calloutElement.contains(clickTarget)
|
|
16805
|
-
) {
|
|
17490
|
+
if (calloutElement.contains(clickTarget)) {
|
|
16806
17491
|
return;
|
|
16807
17492
|
}
|
|
16808
17493
|
if (isInsideOpener(clickTarget)) {
|
|
@@ -16815,14 +17500,20 @@ const openCallout = (
|
|
|
16815
17500
|
debug(event, `click on opener, let its handler decide`);
|
|
16816
17501
|
return;
|
|
16817
17502
|
}
|
|
17503
|
+
// Read before the close, which takes the callout out of the document —
|
|
17504
|
+
// and out of the layer it stands in, for spendsPressOn to find.
|
|
17505
|
+
const spent = spendsPressOn(clickTarget);
|
|
16818
17506
|
requestClose(event, "click_outside");
|
|
17507
|
+
if (spent) {
|
|
17508
|
+
spend(event);
|
|
17509
|
+
}
|
|
16819
17510
|
};
|
|
16820
17511
|
const handleSpaceOutside = (event) => {
|
|
16821
17512
|
if (event.key !== " ") {
|
|
16822
17513
|
return;
|
|
16823
17514
|
}
|
|
16824
17515
|
const keyTarget = event.target;
|
|
16825
|
-
if (
|
|
17516
|
+
if (calloutElement.contains(keyTarget)) {
|
|
16826
17517
|
return;
|
|
16827
17518
|
}
|
|
16828
17519
|
if (isInsideOpener(keyTarget)) {
|
|
@@ -16831,7 +17522,41 @@ const openCallout = (
|
|
|
16831
17522
|
debug(event, `space on opener, let its handler decide`);
|
|
16832
17523
|
return;
|
|
16833
17524
|
}
|
|
17525
|
+
const spent = spendsPressOn(keyTarget);
|
|
16834
17526
|
requestClose(event, "space_outside");
|
|
17527
|
+
if (spent) {
|
|
17528
|
+
spend(event);
|
|
17529
|
+
}
|
|
17530
|
+
};
|
|
17531
|
+
// The press before its click: a click ends a gesture whose pointerdown
|
|
17532
|
+
// already reached the page, and what acts on the press itself — a hold, a
|
|
17533
|
+
// drag, the focus moving — would otherwise happen under a callout that only
|
|
17534
|
+
// closes on the click. Decided at the pointerdown and kept for the whole
|
|
17535
|
+
// press, wherever its later events land: a release beside the callout ends
|
|
17536
|
+
// a drag started inside it, and must reach it.
|
|
17537
|
+
let pressSpent = false;
|
|
17538
|
+
const handlePressOutside = (event) => {
|
|
17539
|
+
if (event.type === "pointerdown") {
|
|
17540
|
+
const pressTarget = event.target;
|
|
17541
|
+
pressSpent =
|
|
17542
|
+
event.button === 0 &&
|
|
17543
|
+
!calloutElement.contains(pressTarget) &&
|
|
17544
|
+
!isInsideOpener(pressTarget) &&
|
|
17545
|
+
spendsPressOn(pressTarget);
|
|
17546
|
+
} else if (event.type === "pointercancel") {
|
|
17547
|
+
// The browser took the press over — a scroll — and nothing follows it.
|
|
17548
|
+
pressSpent = false;
|
|
17549
|
+
return;
|
|
17550
|
+
}
|
|
17551
|
+
if (!pressSpent) {
|
|
17552
|
+
return;
|
|
17553
|
+
}
|
|
17554
|
+
if (event.type === "mousedown" || event.type === "contextmenu") {
|
|
17555
|
+
// What the press does on its own: the focus moving, a selection
|
|
17556
|
+
// starting, the menu of a held finger.
|
|
17557
|
+
event.preventDefault();
|
|
17558
|
+
}
|
|
17559
|
+
event.stopPropagation();
|
|
16835
17560
|
};
|
|
16836
17561
|
|
|
16837
17562
|
const registerClickOutsideListener = () => {
|
|
@@ -16841,6 +17566,16 @@ const openCallout = (
|
|
|
16841
17566
|
document.removeEventListener("click", handleClickOutside, true);
|
|
16842
17567
|
document.removeEventListener("keydown", handleSpaceOutside, true);
|
|
16843
17568
|
});
|
|
17569
|
+
if (backdrop) {
|
|
17570
|
+
for (const type of SPENT_PRESS_EVENT_TYPES) {
|
|
17571
|
+
document.addEventListener(type, handlePressOutside, true);
|
|
17572
|
+
}
|
|
17573
|
+
addTeardown(() => {
|
|
17574
|
+
for (const type of SPENT_PRESS_EVENT_TYPES) {
|
|
17575
|
+
document.removeEventListener(type, handlePressOutside, true);
|
|
17576
|
+
}
|
|
17577
|
+
});
|
|
17578
|
+
}
|
|
16844
17579
|
};
|
|
16845
17580
|
// A callout opened during a press (mousedown or pointerdown) must wait for the
|
|
16846
17581
|
// matching release before listening for click-outside, otherwise the same
|
|
@@ -18045,7 +18780,7 @@ const warnOnReopenWithinSameEvent = (anchorElement, openingEvent) => {
|
|
|
18045
18780
|
*
|
|
18046
18781
|
* Usage:
|
|
18047
18782
|
* const myToken = createOpenToken();
|
|
18048
|
-
* calloutManager.addOpenToken(myToken, { message, status, testId, closeByScroll, anchorElement, event, skipFocus, onClose });
|
|
18783
|
+
* calloutManager.addOpenToken(myToken, { message, status, testId, closeByScroll, backdrop, group, anchorElement, event, skipFocus, onClose });
|
|
18049
18784
|
* calloutManager.removeOpenToken(myToken, event);
|
|
18050
18785
|
* calloutManager.requestCloseCallout(event, debugReason); // force-close all
|
|
18051
18786
|
* calloutManager.callout // current open callout or null
|
|
@@ -18103,6 +18838,8 @@ const createCalloutManager = (
|
|
|
18103
18838
|
closeButton: tokenData.closeButton,
|
|
18104
18839
|
closeByPressOutside: tokenData.status !== "error",
|
|
18105
18840
|
closeByScroll: tokenData.closeByScroll,
|
|
18841
|
+
backdrop: tokenData.backdrop,
|
|
18842
|
+
group: tokenData.group,
|
|
18106
18843
|
anchorElement,
|
|
18107
18844
|
openingEvent: event,
|
|
18108
18845
|
skipFocus: tokenData.skipFocus,
|
|
@@ -18231,6 +18968,8 @@ const createCalloutManager = (
|
|
|
18231
18968
|
icon,
|
|
18232
18969
|
closeButton,
|
|
18233
18970
|
closeByScroll,
|
|
18971
|
+
backdrop,
|
|
18972
|
+
group,
|
|
18234
18973
|
anchorElement,
|
|
18235
18974
|
event,
|
|
18236
18975
|
skipFocus,
|
|
@@ -18248,6 +18987,8 @@ const createCalloutManager = (
|
|
|
18248
18987
|
icon,
|
|
18249
18988
|
closeButton,
|
|
18250
18989
|
closeByScroll,
|
|
18990
|
+
backdrop,
|
|
18991
|
+
group,
|
|
18251
18992
|
skipFocus,
|
|
18252
18993
|
onClose,
|
|
18253
18994
|
// Resolved as the token is added, and kept with it: a token shown later
|
|
@@ -24495,7 +25236,16 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
24495
25236
|
}
|
|
24496
25237
|
return itemSignal;
|
|
24497
25238
|
};
|
|
24498
|
-
|
|
25239
|
+
// A value changes a row when it is another object — the app writing a new
|
|
25240
|
+
// one says something changed, and that is taken as said. An answer says it
|
|
25241
|
+
// with every object nested in its rows: JSON.parse makes them all new, the
|
|
25242
|
+
// ones that did not move included, so a revalidation returning the same
|
|
25243
|
+
// rows would replace every one of them for every reader. What an answer
|
|
25244
|
+
// sends again equal in depth is the value already held (see upsertAnswer).
|
|
25245
|
+
const valueChangedByWrite = (newValue, oldValue) => newValue !== oldValue;
|
|
25246
|
+
const valueChangedByAnswer = (newValue, oldValue) =>
|
|
25247
|
+
newValue !== oldValue && !compareTwoJsValues(newValue, oldValue);
|
|
25248
|
+
const upsertWith = (args, valueChanged) => {
|
|
24499
25249
|
const mutationsMap = new Map(); // Map<itemId, propertyMutations>
|
|
24500
25250
|
const triggerPropertyMutations = () => {
|
|
24501
25251
|
for (const itemPropertiesObserver of itemPropertiesObserverSet) {
|
|
@@ -24559,7 +25309,27 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
24559
25309
|
const newValue = props[key];
|
|
24560
25310
|
if (itemOwnKeys.includes(key)) {
|
|
24561
25311
|
const oldValue = item[key];
|
|
24562
|
-
|
|
25312
|
+
const { set } = itemOwnPropertyDescriptors[key];
|
|
25313
|
+
if (set) {
|
|
25314
|
+
// A relation (.one(), .many() and their scoped forms in
|
|
25315
|
+
// resource_graph.js): the row holds which children it points to,
|
|
25316
|
+
// the children live in their own store, and whoever reads them
|
|
25317
|
+
// follows them there. Its setter writes them and says whether the
|
|
25318
|
+
// row points elsewhere now. Compared as a value instead, the
|
|
25319
|
+
// children received are never the object the row hands out, and
|
|
25320
|
+
// every write naming the relation would replace the row.
|
|
25321
|
+
if (set.call(itemWithProps, newValue)) {
|
|
25322
|
+
hasChanges = true;
|
|
25323
|
+
propertyMutations[key] = {
|
|
25324
|
+
oldValue,
|
|
25325
|
+
newValue,
|
|
25326
|
+
target: item,
|
|
25327
|
+
newTarget: itemWithProps,
|
|
25328
|
+
};
|
|
25329
|
+
}
|
|
25330
|
+
continue;
|
|
25331
|
+
}
|
|
25332
|
+
if (valueChanged(newValue, oldValue)) {
|
|
24563
25333
|
hasChanges = true;
|
|
24564
25334
|
itemWithProps[key] = newValue;
|
|
24565
25335
|
propertyMutations[key] = {
|
|
@@ -24711,6 +25481,9 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
24711
25481
|
triggerPropertyMutations();
|
|
24712
25482
|
return item;
|
|
24713
25483
|
};
|
|
25484
|
+
const upsert = (...args) => upsertWith(args, valueChangedByWrite);
|
|
25485
|
+
// The same, for the result of a callback — what the server answered.
|
|
25486
|
+
const upsertAnswer = (...args) => upsertWith(args, valueChangedByAnswer);
|
|
24714
25487
|
const drop = (...args) => {
|
|
24715
25488
|
const removedItemArray = [];
|
|
24716
25489
|
const triggerRemovedMutations = () => {
|
|
@@ -24887,6 +25660,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
24887
25660
|
selectAll,
|
|
24888
25661
|
itemSignalForId,
|
|
24889
25662
|
upsert,
|
|
25663
|
+
upsertAnswer,
|
|
24890
25664
|
drop,
|
|
24891
25665
|
|
|
24892
25666
|
observeItemProperties,
|
|
@@ -25644,7 +26418,7 @@ const createRangeReader = (
|
|
|
25644
26418
|
`${actionName} must return { items, start, count }, received ${describeResult(result)}.`,
|
|
25645
26419
|
);
|
|
25646
26420
|
}
|
|
25647
|
-
const items = store.
|
|
26421
|
+
const items = store.upsertAnswer(result.items);
|
|
25648
26422
|
let { start, count } = result;
|
|
25649
26423
|
if (start === undefined) {
|
|
25650
26424
|
const startAsked = rangeParams.start;
|
|
@@ -26305,7 +27079,7 @@ const createResource = (
|
|
|
26305
27079
|
: // GET/PUT contract (see .one() JSDoc): the parent object with the
|
|
26306
27080
|
// relationship nested inside, or null for no relationship.
|
|
26307
27081
|
(result) => {
|
|
26308
|
-
const item = store.
|
|
27082
|
+
const item = store.upsertAnswer(result);
|
|
26309
27083
|
const childItem = item[propertyName];
|
|
26310
27084
|
return childItem ? childItem[childIdKey] : undefined;
|
|
26311
27085
|
};
|
|
@@ -26486,8 +27260,8 @@ const createResource = (
|
|
|
26486
27260
|
: (childData) => {
|
|
26487
27261
|
// an array is [property, value, props], used to rename the child id
|
|
26488
27262
|
const childItem = Array.isArray(childData)
|
|
26489
|
-
? childStore.
|
|
26490
|
-
: childStore.
|
|
27263
|
+
? childStore.upsertAnswer(...childData)
|
|
27264
|
+
: childStore.upsertAnswer(childData);
|
|
26491
27265
|
return childItem[childIdKey];
|
|
26492
27266
|
};
|
|
26493
27267
|
const throwInvalidResult = createInvalidResultThrower(
|
|
@@ -26529,7 +27303,7 @@ const createResource = (
|
|
|
26529
27303
|
? (result) => {
|
|
26530
27304
|
// GET_MANY contract (see .many() JSDoc): the parent object with
|
|
26531
27305
|
// the child array nested inside; the array replaces the relationship.
|
|
26532
|
-
const item = store.
|
|
27306
|
+
const item = store.upsertAnswer(result);
|
|
26533
27307
|
const childItemArray = item[propertyName];
|
|
26534
27308
|
return childItemArray.map((childItem) => childItem[childIdKey]);
|
|
26535
27309
|
}
|
|
@@ -26560,7 +27334,7 @@ const createResource = (
|
|
|
26560
27334
|
return deletedChildItemIdArray;
|
|
26561
27335
|
}
|
|
26562
27336
|
: (childDataArray) => {
|
|
26563
|
-
const childItemArray = childStore.
|
|
27337
|
+
const childItemArray = childStore.upsertAnswer(childDataArray);
|
|
26564
27338
|
return childItemArray.map((childItem) => childItem[childIdKey]);
|
|
26565
27339
|
};
|
|
26566
27340
|
const throwInvalidResult = createInvalidResultThrower(
|
|
@@ -26699,18 +27473,37 @@ const createResource = (
|
|
|
26699
27473
|
childSetup(childItem);
|
|
26700
27474
|
}
|
|
26701
27475
|
const childSignal = signal(null);
|
|
27476
|
+
// Returns whether the child changed — what the store asks a relation
|
|
27477
|
+
// (see assign in array_signal_store.js). Assigned in place, the child
|
|
27478
|
+
// tells nobody itself: a prop it receives changed is a change of the
|
|
27479
|
+
// owner's row, which is how its readers see it.
|
|
26702
27480
|
const applyProps = (props) => {
|
|
26703
27481
|
if (!props) {
|
|
27482
|
+
if (childSignal.peek() === null) {
|
|
27483
|
+
return false;
|
|
27484
|
+
}
|
|
26704
27485
|
childSignal.value = null;
|
|
26705
|
-
return;
|
|
27486
|
+
return true;
|
|
26706
27487
|
}
|
|
26707
|
-
|
|
27488
|
+
let changed = false;
|
|
27489
|
+
// Assign each prop in place. Reactive setters (from chained .one()
|
|
27490
|
+
// etc.) fire, and say whether their relation changed.
|
|
26708
27491
|
for (const [key, value] of Object.entries(props)) {
|
|
26709
|
-
|
|
27492
|
+
const descriptor = Object.getOwnPropertyDescriptor(childItem, key);
|
|
27493
|
+
if (descriptor && descriptor.set) {
|
|
27494
|
+
if (descriptor.set.call(childItem, value)) {
|
|
27495
|
+
changed = true;
|
|
27496
|
+
}
|
|
27497
|
+
} else if (childItem[key] !== value) {
|
|
27498
|
+
childItem[key] = value;
|
|
27499
|
+
changed = true;
|
|
27500
|
+
}
|
|
26710
27501
|
}
|
|
26711
27502
|
if (childSignal.peek() !== childItem) {
|
|
26712
27503
|
childSignal.value = childItem; // first activation: null → childItem
|
|
27504
|
+
changed = true;
|
|
26713
27505
|
}
|
|
27506
|
+
return changed;
|
|
26714
27507
|
};
|
|
26715
27508
|
if (!ownerName) {
|
|
26716
27509
|
// Declared on a scoped child, an owner id is only unique inside its
|
|
@@ -26998,7 +27791,7 @@ const createResource = (
|
|
|
26998
27791
|
if (isMany) {
|
|
26999
27792
|
// GET_MANY, POST_MANY, PUT_MANY etc: rest[0] is the array of items,
|
|
27000
27793
|
// and it replaces the whole collection.
|
|
27001
|
-
const itemArray = childStore.
|
|
27794
|
+
const itemArray = childStore.upsertAnswer(rest[0]);
|
|
27002
27795
|
const idArray = itemArray.map((childItem) => childItem[childIdKey]);
|
|
27003
27796
|
idArraySignal.value = idArray;
|
|
27004
27797
|
return [ownerId, idArray];
|
|
@@ -27007,8 +27800,8 @@ const createResource = (
|
|
|
27007
27800
|
// GET, POST, PUT, PATCH: rest may be [props] or [oldId, props] for renames
|
|
27008
27801
|
const childItem =
|
|
27009
27802
|
rest.length > 1
|
|
27010
|
-
? childStore.
|
|
27011
|
-
: childStore.
|
|
27803
|
+
? childStore.upsertAnswer(...rest)
|
|
27804
|
+
: childStore.upsertAnswer(rest[0]);
|
|
27012
27805
|
return [ownerId, childItem[childIdKey]];
|
|
27013
27806
|
},
|
|
27014
27807
|
valueToData: (value) => {
|
|
@@ -27144,8 +27937,8 @@ const createRestActionFactoryForRoot = (
|
|
|
27144
27937
|
// An array result is [property, value, props] — used to rename the
|
|
27145
27938
|
// idKey of an item: store.upsert("name", "currentName", { name: "newName" })
|
|
27146
27939
|
const item = Array.isArray(result)
|
|
27147
|
-
? store.
|
|
27148
|
-
: store.
|
|
27940
|
+
? store.upsertAnswer(...result)
|
|
27941
|
+
: store.upsertAnswer(result);
|
|
27149
27942
|
return item[idKey];
|
|
27150
27943
|
};
|
|
27151
27944
|
const throwInvalidResult = createInvalidResultThrower(
|
|
@@ -27209,7 +28002,7 @@ const createRestActionFactoryForRoot = (
|
|
|
27209
28002
|
verb === "DELETE"
|
|
27210
28003
|
? (idOrMutableIdArray) => store.drop(idOrMutableIdArray)
|
|
27211
28004
|
: (dataArray) => {
|
|
27212
|
-
const itemArray = store.
|
|
28005
|
+
const itemArray = store.upsertAnswer(dataArray);
|
|
27213
28006
|
return itemArray.map((item) => item[idKey]);
|
|
27214
28007
|
};
|
|
27215
28008
|
|
|
@@ -27338,14 +28131,17 @@ ${relationLabel} source location: ${declarationSite}`,
|
|
|
27338
28131
|
// array of child props/ids, or anything else meaning "empty") into an array of
|
|
27339
28132
|
// child ids, upserting each entry into the child store. The id array signal is
|
|
27340
28133
|
// only touched when the resulting ids actually differ.
|
|
28134
|
+
// Returns whether the children the row points to changed — what the store asks
|
|
28135
|
+
// a relation (see assign in array_signal_store.js).
|
|
27341
28136
|
const createChildIdArrayUpdater = (childStore, childIdKey, idArraySignal) => {
|
|
27342
28137
|
return (valueArray) => {
|
|
27343
28138
|
const currentIdArray = idArraySignal.peek();
|
|
27344
28139
|
if (!Array.isArray(valueArray)) {
|
|
27345
28140
|
if (currentIdArray.length > 0) {
|
|
27346
28141
|
idArraySignal.value = [];
|
|
28142
|
+
return true;
|
|
27347
28143
|
}
|
|
27348
|
-
return;
|
|
28144
|
+
return false;
|
|
27349
28145
|
}
|
|
27350
28146
|
const idArray = [];
|
|
27351
28147
|
let modified = false;
|
|
@@ -27367,7 +28163,9 @@ const createChildIdArrayUpdater = (childStore, childIdKey, idArraySignal) => {
|
|
|
27367
28163
|
}
|
|
27368
28164
|
if (modified || currentIdArray.length !== idArray.length) {
|
|
27369
28165
|
idArraySignal.value = idArray;
|
|
28166
|
+
return true;
|
|
27370
28167
|
}
|
|
28168
|
+
return false;
|
|
27371
28169
|
};
|
|
27372
28170
|
};
|
|
27373
28171
|
|
|
@@ -27785,6 +28583,24 @@ const TYPE_CONVERTERS = {
|
|
|
27785
28583
|
},
|
|
27786
28584
|
};
|
|
27787
28585
|
|
|
28586
|
+
/**
|
|
28587
|
+
* What a component can learn about the page it is rendered in, from the route
|
|
28588
|
+
* container rendering that page (see useContentKeepingLeavingPages in
|
|
28589
|
+
* route.jsx). `null` outside of any route container.
|
|
28590
|
+
*
|
|
28591
|
+
* - `isShown()` — whether the container would still show the page. False as
|
|
28592
|
+
* soon as the address leads elsewhere: the page is about to be taken down, or
|
|
28593
|
+
* hidden until the route transition leaving it is over, and nothing it reads
|
|
28594
|
+
* is worth a render any more. False for good once it is kept while leaving: a
|
|
28595
|
+
* return to its route mounts the page anew. Asked outside any render — by a
|
|
28596
|
+
* subscription deciding whether to re-render — and it reads the routes
|
|
28597
|
+
* untracked, so asking subscribes nobody to them.
|
|
28598
|
+
* - `leavingSignal` — true once the page is hidden and kept while its movement
|
|
28599
|
+
* plays: still mounted, so what it fills outside its own nodes (a slot in a
|
|
28600
|
+
* bar) steps out rather than showing with the page arriving.
|
|
28601
|
+
*/
|
|
28602
|
+
const RoutePageContext = createContext(null);
|
|
28603
|
+
|
|
27788
28604
|
const promiseStateWeakMap = new WeakMap();
|
|
27789
28605
|
const usePromiseAsyncData = (
|
|
27790
28606
|
promise,
|
|
@@ -27920,12 +28736,39 @@ const useActionAsyncData = (action, {
|
|
|
27920
28736
|
// has already processed the settlement and the detached DOM is discarded.
|
|
27921
28737
|
const runningState = action.runningStateSignal.peek();
|
|
27922
28738
|
const [, setTick] = useState(0);
|
|
28739
|
+
const routePage = useContext(RoutePageContext);
|
|
27923
28740
|
useEffect(() => {
|
|
28741
|
+
let stopOwingRender = null;
|
|
28742
|
+
const rerender = () => {
|
|
28743
|
+
// Not for a page the address has left: it is about to be taken down,
|
|
28744
|
+
// or hidden until the route transition leaving it is over, and its
|
|
28745
|
+
// route's action moving as that route stops matching would re-render
|
|
28746
|
+
// the whole page — inside the transition's update callback, where
|
|
28747
|
+
// every render delays the movement (see route_page_context.js).
|
|
28748
|
+
if (routePage && !routePage.isShown()) {
|
|
28749
|
+
// Owed rather than dropped: the address can come back before the
|
|
28750
|
+
// container renders (a redirect straight back), and the page then
|
|
28751
|
+
// stays on screen with what it read last.
|
|
28752
|
+
if (!stopOwingRender) {
|
|
28753
|
+
stopOwingRender = observeRouteRender(() => {
|
|
28754
|
+
if (routePage.isShown()) {
|
|
28755
|
+
rerender();
|
|
28756
|
+
}
|
|
28757
|
+
});
|
|
28758
|
+
}
|
|
28759
|
+
return;
|
|
28760
|
+
}
|
|
28761
|
+
if (stopOwingRender) {
|
|
28762
|
+
stopOwingRender();
|
|
28763
|
+
stopOwingRender = null;
|
|
28764
|
+
}
|
|
28765
|
+
setTick(n => n + 1);
|
|
28766
|
+
};
|
|
27924
28767
|
const unsubscribeFromRunningState = action.runningStateSignal.subscribe(state => {
|
|
27925
28768
|
if (state === RUNNING) {
|
|
27926
28769
|
dismissedActionWeakSet.delete(action);
|
|
27927
28770
|
}
|
|
27928
|
-
|
|
28771
|
+
rerender();
|
|
27929
28772
|
});
|
|
27930
28773
|
// The data does not come from this action's runs alone: dataSignal is a
|
|
27931
28774
|
// computed over the resource store, so an other action writing that store
|
|
@@ -27939,7 +28782,7 @@ const useActionAsyncData = (action, {
|
|
|
27939
28782
|
dataNotificationIsInitial = false;
|
|
27940
28783
|
return;
|
|
27941
28784
|
}
|
|
27942
|
-
|
|
28785
|
+
rerender();
|
|
27943
28786
|
});
|
|
27944
28787
|
// The params say WHICH question this is, and this hook reads them: to know
|
|
27945
28788
|
// there is nothing to ask for, and to start the run it owns. A binding
|
|
@@ -27952,7 +28795,7 @@ const useActionAsyncData = (action, {
|
|
|
27952
28795
|
paramsNotificationIsInitial = false;
|
|
27953
28796
|
return;
|
|
27954
28797
|
}
|
|
27955
|
-
|
|
28798
|
+
rerender();
|
|
27956
28799
|
});
|
|
27957
28800
|
// A debounced binding waits before it retargets, so nothing above changes
|
|
27958
28801
|
// while the delay runs — but what is on screen is already out of date.
|
|
@@ -27962,18 +28805,21 @@ const useActionAsyncData = (action, {
|
|
|
27962
28805
|
settlingNotificationIsInitial = false;
|
|
27963
28806
|
return;
|
|
27964
28807
|
}
|
|
27965
|
-
|
|
28808
|
+
rerender();
|
|
27966
28809
|
});
|
|
27967
28810
|
return () => {
|
|
27968
28811
|
unsubscribeFromRunningState();
|
|
27969
28812
|
unsubscribeFromData();
|
|
27970
28813
|
unsubscribeFromParams();
|
|
27971
28814
|
unsubscribeFromParamsSettling();
|
|
28815
|
+
if (stopOwingRender) {
|
|
28816
|
+
stopOwingRender();
|
|
28817
|
+
}
|
|
27972
28818
|
};
|
|
27973
28819
|
// Bound to the action, not to the mount: params given as a plain object
|
|
27974
28820
|
// make another action instance, and the component would otherwise stay
|
|
27975
28821
|
// subscribed to the state of the one it no longer reads.
|
|
27976
|
-
}, [action]);
|
|
28822
|
+
}, [action, routePage]);
|
|
27977
28823
|
|
|
27978
28824
|
// The params moved and the run has not started: under a debounce the action
|
|
27979
28825
|
// is still the previous one, COMPLETED with the previous answer. There is no
|
|
@@ -32133,13 +32979,6 @@ const keepLeavingPages = () => {
|
|
|
32133
32979
|
hold.containers.clear();
|
|
32134
32980
|
};
|
|
32135
32981
|
};
|
|
32136
|
-
|
|
32137
|
-
// What a page can learn about its place from the container rendering it:
|
|
32138
|
-
// whether it is a page kept while leaving (see keepLeavingPages). Such a page
|
|
32139
|
-
// is still mounted, and whatever it fills OUTSIDE its own nodes — a slot in a
|
|
32140
|
-
// bar, say — would otherwise go on showing with the page arriving (see
|
|
32141
|
-
// layout/slot.jsx).
|
|
32142
|
-
const RoutePageContext = createContext(null);
|
|
32143
32982
|
const LEAVING_ATTRIBUTE = "data-navi-route-leaving";
|
|
32144
32983
|
const css$13 = /* css */`[data-navi-route-leaving] {
|
|
32145
32984
|
display: none !important;
|
|
@@ -32264,7 +33103,21 @@ const RouteContainer = ({
|
|
|
32264
33103
|
shownBranchRef.current = activeBranch;
|
|
32265
33104
|
}
|
|
32266
33105
|
const branch = shownBranchRef.current || activeBranch;
|
|
32267
|
-
|
|
33106
|
+
// What this container would show if it rendered now, by the same rule as
|
|
33107
|
+
// the render above — asked from outside any render (see isShown below).
|
|
33108
|
+
const childrenRef = useRef(null);
|
|
33109
|
+
childrenRef.current = children;
|
|
33110
|
+
const readShownBranchRef = useRef(null);
|
|
33111
|
+
if (!readShownBranchRef.current) {
|
|
33112
|
+
readShownBranchRef.current = () => {
|
|
33113
|
+
if (routeRenderFrozen) {
|
|
33114
|
+
return shownBranchRef.current;
|
|
33115
|
+
}
|
|
33116
|
+
return collectBranches(childrenRef.current).activeBranch;
|
|
33117
|
+
};
|
|
33118
|
+
}
|
|
33119
|
+
const parentPage = useContext(RoutePageContext);
|
|
33120
|
+
const content = useContentKeepingLeavingPages(branch, readShownBranchRef.current, parentPage);
|
|
32268
33121
|
|
|
32269
33122
|
// The two things this component knows that nobody outside can find out: the
|
|
32270
33123
|
// pages a named fallback is the absence of — the children it was written
|
|
@@ -32301,7 +33154,7 @@ const RouteContainer = ({
|
|
|
32301
33154
|
// page being left must stay the child it was when a page is added beside it,
|
|
32302
33155
|
// or Preact takes it down and builds it again.
|
|
32303
33156
|
let leavingKeyCount = 0;
|
|
32304
|
-
const useContentKeepingLeavingPages = branch => {
|
|
33157
|
+
const useContentKeepingLeavingPages = (branch, readShownBranch, parentPage) => {
|
|
32305
33158
|
const [, rerender] = useReducer(countRenders, 0);
|
|
32306
33159
|
const shownRef = useRef(null);
|
|
32307
33160
|
const leavingRef = useRef([]);
|
|
@@ -32319,6 +33172,7 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
32319
33172
|
// of reach — the page arriving could not even take the focus — for as long
|
|
32320
33173
|
// as it is open, and one in a page kept while leaving stays open for the
|
|
32321
33174
|
// whole movement. The page is then taken down at once, as without one.
|
|
33175
|
+
shown.page.left = true;
|
|
32322
33176
|
leavingRef.current.push({
|
|
32323
33177
|
vnode: shown.vnode,
|
|
32324
33178
|
page: shown.page,
|
|
@@ -32335,10 +33189,11 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
32335
33189
|
let vnode = null;
|
|
32336
33190
|
if (branch) {
|
|
32337
33191
|
if (!page) {
|
|
32338
|
-
page =
|
|
32339
|
-
leavingSignal: signal(false)
|
|
32340
|
-
};
|
|
33192
|
+
page = createRoutePage(readShownBranch, parentPage);
|
|
32341
33193
|
}
|
|
33194
|
+
// A page carried over follows the branch it is now the page of.
|
|
33195
|
+
page.branchId = branch.id;
|
|
33196
|
+
page.element = element;
|
|
32342
33197
|
vnode = h(RoutePageContext.Provider, {
|
|
32343
33198
|
key,
|
|
32344
33199
|
value: page
|
|
@@ -32405,6 +33260,33 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
32405
33260
|
return children;
|
|
32406
33261
|
};
|
|
32407
33262
|
const countRenders = count => count + 1;
|
|
33263
|
+
// The page a container renders, as RoutePageContext hands it over (see
|
|
33264
|
+
// route_page_context.js). Shown while the container would pick its branch, or
|
|
33265
|
+
// a branch rendering the same element (it is then carried over), and while the
|
|
33266
|
+
// page it sits in is shown itself. Never again once kept while leaving: coming
|
|
33267
|
+
// back to its route mounts the page anew.
|
|
33268
|
+
const createRoutePage = (readShownBranch, parentPage) => {
|
|
33269
|
+
const page = {
|
|
33270
|
+
branchId: null,
|
|
33271
|
+
element: undefined,
|
|
33272
|
+
left: false,
|
|
33273
|
+
leavingSignal: signal(false),
|
|
33274
|
+
isShown: () => {
|
|
33275
|
+
if (page.left) {
|
|
33276
|
+
return false;
|
|
33277
|
+
}
|
|
33278
|
+
if (parentPage && !parentPage.isShown()) {
|
|
33279
|
+
return false;
|
|
33280
|
+
}
|
|
33281
|
+
const shownBranch = untracked(readShownBranch);
|
|
33282
|
+
if (!shownBranch) {
|
|
33283
|
+
return false;
|
|
33284
|
+
}
|
|
33285
|
+
return shownBranch.id === page.branchId || branchElement(shownBranch) === page.element;
|
|
33286
|
+
}
|
|
33287
|
+
};
|
|
33288
|
+
return page;
|
|
33289
|
+
};
|
|
32408
33290
|
// What Preact compares when a container changes page, below the <Route> it
|
|
32409
33291
|
// renders either way: the element the page is rendered with — a layout, for a
|
|
32410
33292
|
// container branch.
|
|
@@ -38777,854 +39659,318 @@ registerNaviCommand(
|
|
|
38777
39659
|
const target =
|
|
38778
39660
|
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38779
39661
|
if (!target) {
|
|
38780
|
-
return undefined;
|
|
38781
|
-
}
|
|
38782
|
-
return {
|
|
38783
|
-
target,
|
|
38784
|
-
implementation: () => {
|
|
38785
|
-
const isExpanded = target.getAttribute("aria-expanded") === "true";
|
|
38786
|
-
const customEventName = isExpanded
|
|
38787
|
-
? "navi_request_close"
|
|
38788
|
-
: "navi_request_open";
|
|
38789
|
-
return dispatchCustomEvent(target, customEventName, {
|
|
38790
|
-
event,
|
|
38791
|
-
source: resolveCommandProxySource(source),
|
|
38792
|
-
anchor,
|
|
38793
|
-
// Same as --navi-open below: the half of the toggle that opens says
|
|
38794
|
-
// what it opens ON, and the half that closes carries it too so the
|
|
38795
|
-
// popup never has to ask which half it just heard.
|
|
38796
|
-
value:
|
|
38797
|
-
value === undefined ? resolveCommandValue(source, event) : value,
|
|
38798
|
-
});
|
|
38799
|
-
},
|
|
38800
|
-
};
|
|
38801
|
-
},
|
|
38802
|
-
);
|
|
38803
|
-
// A popup that edits opens ON something, and the press is the only place that
|
|
38804
|
-
// knows which one. It says it with `value` — "what this is about", as
|
|
38805
|
-
// everywhere else — rather than with an argument, which says what the command
|
|
38806
|
-
// DOES: "open" is already a complete instruction, unlike --navi-go-to-slide.
|
|
38807
|
-
// <Button value={radar.id} command="--navi-open" commandfor="radar-dialog">
|
|
38808
|
-
// The popup is told before it opens — see Dialog/Popover's `onOpen`.
|
|
38809
|
-
registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
|
|
38810
|
-
const target =
|
|
38811
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38812
|
-
if (!target) {
|
|
38813
|
-
return undefined;
|
|
38814
|
-
}
|
|
38815
|
-
return {
|
|
38816
|
-
target,
|
|
38817
|
-
implementation: () => {
|
|
38818
|
-
// The popup reads `anchor` first and falls back to the source (see
|
|
38819
|
-
// onnavi_request_open in popover.jsx): without an `anchor`, the element
|
|
38820
|
-
// asking is the element opened against.
|
|
38821
|
-
return dispatchCustomEvent(target, "navi_request_open", {
|
|
38822
|
-
event,
|
|
38823
|
-
source: resolveCommandProxySource(source),
|
|
38824
|
-
anchor,
|
|
38825
|
-
value: value === undefined ? resolveCommandValue(source, event) : value,
|
|
38826
|
-
});
|
|
38827
|
-
},
|
|
38828
|
-
};
|
|
38829
|
-
});
|
|
38830
|
-
/**
|
|
38831
|
-
* Tells the popup a source opens that a press on it has started, ahead of the
|
|
38832
|
-
* release that runs the command: what the opening will read can be asked for
|
|
38833
|
-
* now (see open_controller.js). A press rather than a hover, because what
|
|
38834
|
-
* that reads is keyed by the popup — one sheet per card — and a pointer
|
|
38835
|
-
* crossing a list of cards has chosen none of them; on a touch screen the
|
|
38836
|
-
* press is the first thing the finger says anyway.
|
|
38837
|
-
*
|
|
38838
|
-
* The target is found the way --navi-open finds it, but silently: nothing
|
|
38839
|
-
* runs here, and a source with no target says so at the release.
|
|
38840
|
-
*/
|
|
38841
|
-
const announceOpeningPress = (source, command) => {
|
|
38842
|
-
if (command !== "--navi-open" && command !== "--navi-toggle") {
|
|
38843
|
-
return;
|
|
38844
|
-
}
|
|
38845
|
-
const commandFor = source.getAttribute("commandfor");
|
|
38846
|
-
const target = commandFor
|
|
38847
|
-
? document.getElementById(commandFor)
|
|
38848
|
-
: resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38849
|
-
if (!target) {
|
|
38850
|
-
return;
|
|
38851
|
-
}
|
|
38852
|
-
dispatchCustomEvent(target, "navi_open_press");
|
|
38853
|
-
};
|
|
38854
|
-
// "--navi-close:all" closes every expandable above the source, nearest first —
|
|
38855
|
-
// a link leaving from a badge shown over a sheet leaves both. A surface that
|
|
38856
|
-
// refuses (a form asking about its changes) keeps what is above it open too:
|
|
38857
|
-
// one cannot be out of the sheet while still in the badge.
|
|
38858
|
-
registerNaviCommand("--navi-close", (source, event, { argument }) => {
|
|
38859
|
-
const target =
|
|
38860
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38861
|
-
if (!target) {
|
|
38862
|
-
return undefined;
|
|
38863
|
-
}
|
|
38864
|
-
return {
|
|
38865
|
-
target,
|
|
38866
|
-
implementation: () => {
|
|
38867
|
-
const detail = { event, source: resolveCommandProxySource(source) };
|
|
38868
|
-
if (argument === "all") {
|
|
38869
|
-
return requestCloseUpward(target, detail);
|
|
38870
|
-
}
|
|
38871
|
-
return dispatchCustomEvent(target, "navi_request_close", detail);
|
|
38872
|
-
},
|
|
38873
|
-
};
|
|
38874
|
-
});
|
|
38875
|
-
const requestCloseUpward = (target, detail) => {
|
|
38876
|
-
let expandable = target;
|
|
38877
|
-
while (expandable) {
|
|
38878
|
-
const closing = dispatchCustomEvent(
|
|
38879
|
-
expandable,
|
|
38880
|
-
"navi_request_close",
|
|
38881
|
-
detail,
|
|
38882
|
-
);
|
|
38883
|
-
if (!closing) {
|
|
38884
|
-
return false;
|
|
38885
|
-
}
|
|
38886
|
-
const parent = expandable.parentElement;
|
|
38887
|
-
expandable = parent ? parent.closest("[aria-expanded]") : null;
|
|
38888
|
-
}
|
|
38889
|
-
return true;
|
|
38890
|
-
};
|
|
38891
|
-
registerNaviCommand("--navi-cancel", (source, event) => {
|
|
38892
|
-
const target =
|
|
38893
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38894
|
-
if (!target) {
|
|
38895
|
-
return undefined;
|
|
38896
|
-
}
|
|
38897
|
-
return {
|
|
38898
|
-
target,
|
|
38899
|
-
implementation: () => {
|
|
38900
|
-
return dispatchCustomEvent(target, "navi_request_close", {
|
|
38901
|
-
event,
|
|
38902
|
-
source: resolveCommandProxySource(source),
|
|
38903
|
-
isCancel: true,
|
|
38904
|
-
});
|
|
38905
|
-
},
|
|
38906
|
-
};
|
|
38907
|
-
});
|
|
38908
|
-
// "Yes, do it" — said inside the popup of a confirm picker (see
|
|
38909
|
-
// picker/picker_confirm.jsx), by its own yes button or by whatever a caller put
|
|
38910
|
-
// there instead. Its counterpart is --navi-cancel; the difference between the
|
|
38911
|
-
// two is this event, since both then close the popup, and a popup that closes
|
|
38912
|
-
// without having said this is the answer "no".
|
|
38913
|
-
registerNaviCommand("--navi-confirm", (source, event) => {
|
|
38914
|
-
const target =
|
|
38915
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38916
|
-
if (!target) {
|
|
38917
|
-
return undefined;
|
|
38918
|
-
}
|
|
38919
|
-
return {
|
|
38920
|
-
target,
|
|
38921
|
-
implementation: () => {
|
|
38922
|
-
const commandSource = resolveCommandProxySource(source);
|
|
38923
|
-
dispatchCustomEvent(target, "navi_request_confirm", {
|
|
38924
|
-
event,
|
|
38925
|
-
source: commandSource,
|
|
38926
|
-
});
|
|
38927
|
-
return dispatchCustomEvent(target, "navi_request_close", {
|
|
38928
|
-
event,
|
|
38929
|
-
source: commandSource,
|
|
38930
|
-
});
|
|
38931
|
-
},
|
|
38932
|
-
};
|
|
38933
|
-
});
|
|
38934
|
-
registerNaviCommand("--navi-define", (source, event) => {
|
|
38935
|
-
const target =
|
|
38936
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38937
|
-
if (!target) {
|
|
38938
|
-
return undefined;
|
|
38939
|
-
}
|
|
38940
|
-
return {
|
|
38941
|
-
target,
|
|
38942
|
-
implementation: () => executeNaviDefine(source, event, target),
|
|
38943
|
-
};
|
|
38944
|
-
});
|
|
38945
|
-
// A send whose target is the popup itself: what is around the source IS the
|
|
38946
|
-
// popup's answer, and a picker runs its action with that answer as it closes
|
|
38947
|
-
// (see onRequestClose in picker_custom.jsx) — dispatching the action here too
|
|
38948
|
-
// would run it twice. `isSend` tells this close from a dismissal (the cross,
|
|
38949
|
-
// a press outside, Escape): the person pressed a choice, which a picker may
|
|
38950
|
-
// act on even when the choice is the one it already held.
|
|
38951
|
-
const executeNaviDefine = (source, event, target) => {
|
|
38952
|
-
return dispatchCustomEvent(target, "navi_request_close", {
|
|
38953
|
-
event,
|
|
38954
|
-
source: resolveCommandProxySource(source),
|
|
38955
|
-
isSend: true,
|
|
38956
|
-
});
|
|
38957
|
-
};
|
|
38958
|
-
|
|
38959
|
-
registerNaviCommand("--navi-scroll", (source, event) => {
|
|
38960
|
-
const target =
|
|
38961
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38962
|
-
if (!target) {
|
|
38963
|
-
return undefined;
|
|
38964
|
-
}
|
|
38965
|
-
return {
|
|
38966
|
-
target,
|
|
38967
|
-
implementation: () => {
|
|
38968
|
-
return dispatchCustomEvent(target, "navi_request_scroll", {
|
|
38969
|
-
event,
|
|
38970
|
-
id: resolveCommandValue(source, event),
|
|
38971
|
-
});
|
|
38972
|
-
},
|
|
38973
|
-
};
|
|
38974
|
-
});
|
|
38975
|
-
// The press a hand is still making, given to something that can be carried: a
|
|
38976
|
-
// hold opened a popup, and what the popup holds is to follow that same finger
|
|
38977
|
-
// without it lifting and pressing again. `event` is what the press set off (the
|
|
38978
|
-
// popup's open event), and the press is read from its chain. Run once the popup
|
|
38979
|
-
// has settled, which the caller cannot see: the carry measures its element and
|
|
38980
|
-
// puts a copy in the top layer, neither of which holds while a transition is
|
|
38981
|
-
// still moving the box (see afterPopupLift). What the carry then is — its
|
|
38982
|
-
// outcomes, its refusal — is the element's (see interaction_drag.js).
|
|
38983
|
-
registerNaviCommand("--navi-grab", (source, event) => {
|
|
38984
|
-
const target =
|
|
38985
|
-
resolveExplicitTarget(source) || source.closest("[data-drag-source]");
|
|
38986
|
-
if (!target) {
|
|
38987
|
-
return undefined;
|
|
38988
|
-
}
|
|
38989
|
-
return {
|
|
38990
|
-
target,
|
|
38991
|
-
implementation: () => {
|
|
38992
|
-
afterPopupLift(() => {
|
|
38993
|
-
// Taken away with the popup before it settled.
|
|
38994
|
-
if (!target.isConnected) {
|
|
38995
|
-
return;
|
|
38996
|
-
}
|
|
38997
|
-
dispatchCustomEvent(target, "navi_request_grab", { event });
|
|
38998
|
-
});
|
|
38999
|
-
return true;
|
|
39000
|
-
},
|
|
39001
|
-
};
|
|
39002
|
-
});
|
|
39003
|
-
registerNaviCommand("--navi-check", (source, event) => {
|
|
39004
|
-
const target =
|
|
39005
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39006
|
-
if (!target) {
|
|
39007
|
-
return undefined;
|
|
39008
|
-
}
|
|
39009
|
-
return {
|
|
39010
|
-
target,
|
|
39011
|
-
implementation: () => {
|
|
39012
|
-
return dispatchCustomEvent(target, "navi_request_check", {
|
|
39013
|
-
event,
|
|
39014
|
-
});
|
|
39015
|
-
},
|
|
39016
|
-
};
|
|
39017
|
-
});
|
|
39018
|
-
registerNaviCommand("--navi-uncheck", (source, event) => {
|
|
39019
|
-
const target =
|
|
39020
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39021
|
-
if (!target) {
|
|
39022
|
-
return undefined;
|
|
39023
|
-
}
|
|
39024
|
-
return {
|
|
39025
|
-
target,
|
|
39026
|
-
implementation: () => {
|
|
39027
|
-
return dispatchCustomEvent(target, "navi_request_uncheck", {
|
|
39028
|
-
event,
|
|
39029
|
-
});
|
|
39030
|
-
},
|
|
39031
|
-
};
|
|
39032
|
-
});
|
|
39033
|
-
registerNaviCommand("--navi-select", (source, event) => {
|
|
39034
|
-
const target =
|
|
39035
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39036
|
-
if (!target) {
|
|
39037
|
-
return undefined;
|
|
39038
|
-
}
|
|
39039
|
-
return {
|
|
39040
|
-
target,
|
|
39041
|
-
implementation: () => {
|
|
39042
|
-
return dispatchCustomEvent(target, "navi_request_select", {
|
|
39043
|
-
event,
|
|
39044
|
-
id: resolveCommandValue(source, event),
|
|
39045
|
-
});
|
|
39046
|
-
},
|
|
39047
|
-
};
|
|
39048
|
-
});
|
|
39049
|
-
registerNaviCommand("--navi-unselect", (source, event) => {
|
|
39050
|
-
const target =
|
|
39051
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39052
|
-
if (!target) {
|
|
39053
|
-
return undefined;
|
|
39054
|
-
}
|
|
39055
|
-
return {
|
|
39056
|
-
target,
|
|
39057
|
-
implementation: () => {
|
|
39058
|
-
return dispatchCustomEvent(target, "navi_request_unselect", {
|
|
39059
|
-
event,
|
|
39060
|
-
id: resolveCommandValue(source, event),
|
|
39061
|
-
});
|
|
39062
|
-
},
|
|
39063
|
-
};
|
|
39064
|
-
});
|
|
39065
|
-
|
|
39066
|
-
/**
|
|
39067
|
-
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
39068
|
-
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
39069
|
-
* mounted run inside the call too, exactly as they would on any other commit.
|
|
39070
|
-
*
|
|
39071
|
-
* For the caller that has to read the DOM it just asked for — measuring an
|
|
39072
|
-
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
39073
|
-
* to do it, because what comes after is a browser event still in flight
|
|
39074
|
-
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
39075
|
-
*
|
|
39076
|
-
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
39077
|
-
* render queue drains; swapping it for "right now" for the duration of the call
|
|
39078
|
-
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
39079
|
-
* case above: rendering synchronously in the middle of an event gives up the
|
|
39080
|
-
* batching that makes several state changes one commit.
|
|
39081
|
-
*/
|
|
39082
|
-
const flushSyncRendering = (fn) => {
|
|
39083
|
-
const debounceRenderingPrevious = options.debounceRendering;
|
|
39084
|
-
options.debounceRendering = (drainRenderQueue) => {
|
|
39085
|
-
drainRenderQueue();
|
|
39086
|
-
};
|
|
39087
|
-
try {
|
|
39088
|
-
fn();
|
|
39089
|
-
} finally {
|
|
39090
|
-
options.debounceRendering = debounceRenderingPrevious;
|
|
39091
|
-
}
|
|
39092
|
-
};
|
|
39093
|
-
|
|
39094
|
-
/**
|
|
39095
|
-
* Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
|
|
39096
|
-
* (non-top-layer) renderers — operate on a plain DOM element, no knowledge
|
|
39097
|
-
* of which of the two owns it.
|
|
39098
|
-
*/
|
|
39099
|
-
|
|
39100
|
-
|
|
39101
|
-
/**
|
|
39102
|
-
* Whether a visibleRectEffect delivery is one that can have taken height away
|
|
39103
|
-
* from a popup, and so pushed whatever holds focus out of sight:
|
|
39104
|
-
* - "resize": the window/visual viewport settled — which is also how the
|
|
39105
|
-
* on-screen keyboard arrives, overlay or not (window_size.js);
|
|
39106
|
-
* - ELEMENT_SIZE_CHANGE: the popup's own box measured different;
|
|
39107
|
-
* - "focusin": the focus-settled re-measure, for the room that changes with
|
|
39108
|
-
* nothing announcing it (subscribeFocusSettled in window_size.js).
|
|
39109
|
-
*
|
|
39110
|
-
* Everything else is a scroll of one kind or another, where nothing got
|
|
39111
|
-
* smaller and scrolling the focused element back would fight the very gesture
|
|
39112
|
-
* that fired it.
|
|
39113
|
-
*/
|
|
39114
|
-
const mayHaveHiddenFocus = (event) => {
|
|
39115
|
-
const type = event?.type;
|
|
39116
|
-
return (
|
|
39117
|
-
type === "resize" || type === ELEMENT_SIZE_CHANGE || type === "focusin"
|
|
39118
|
-
);
|
|
39119
|
-
};
|
|
39120
|
-
|
|
39121
|
-
const scrollportHeightMap = new WeakMap();
|
|
39122
|
-
/**
|
|
39123
|
-
* Scrolls whatever holds focus inside `popupEl` back into view, if the popup
|
|
39124
|
-
* getting shorter has pushed it out.
|
|
39125
|
-
*
|
|
39126
|
-
* The case this exists for: a field low in the scrolling body of a popup that
|
|
39127
|
-
* also has a footer (box.jsx — with a body, the body is the only thing that
|
|
39128
|
-
* scrolls and the footer is a sibling sitting right under it). Focusing the
|
|
39129
|
-
* field makes the browser scroll it into view, which it does against the
|
|
39130
|
-
* popup's height AT THAT MOMENT; the on-screen keyboard then opens and takes
|
|
39131
|
-
* that height away. The body shrinks, its scrollTop does not move, so the
|
|
39132
|
-
* content slides down relative to the shorter scrollport and the field ends up
|
|
39133
|
-
* past its bottom edge — visually, swallowed by the footer. The browser does
|
|
39134
|
-
* not redo a scroll-into-view it already answered, so this does.
|
|
39135
|
-
*
|
|
39136
|
-
* Only what the shrink itself hid, though: with the keyboard up the user reads
|
|
39137
|
-
* the rest of the popup by scrolling the field away — to reach the submit under
|
|
39138
|
-
* it, typically — and the room keeps changing while they do (a keyboard settling
|
|
39139
|
-
* in two steps, a suggestion strip, a browser bar). Answering each of those by
|
|
39140
|
-
* scrolling the field back takes the popup away from wherever they had just
|
|
39141
|
-
* scrolled it, over and over: what they were reading cannot be reached at all
|
|
39142
|
-
* without blurring the field first, and the popup reads as unscrollable. So the
|
|
39143
|
-
* field is brought back only when it was in view before the room shrank, which
|
|
39144
|
-
* one remembered number answers: a resize moves neither scrollTop nor the
|
|
39145
|
-
* element's offset inside the scrolled content, so measuring against the height
|
|
39146
|
-
* the scrollport HAD is measuring the state before the change.
|
|
39147
|
-
*
|
|
39148
|
-
* Scoped to the field's own scroll container (never the page): a popup traps
|
|
39149
|
-
* scrolling precisely so the document underneath cannot move, and a plain
|
|
39150
|
-
* scrollIntoView walks past a container whose scrollbar isn't visible — see
|
|
39151
|
-
* scrollIntoViewScoped's own doc.
|
|
39152
|
-
*
|
|
39153
|
-
* "nearest": the smallest scroll that makes it visible, and none at all when it
|
|
39154
|
-
* already is. Where it lands is the container's own business — a navi scroller
|
|
39155
|
-
* keeps a band free at its edges so a field never comes back glued to one (see
|
|
39156
|
-
* scroll-padding in box.jsx).
|
|
39157
|
-
*/
|
|
39158
|
-
const keepFocusedElementVisible = (popupEl) => {
|
|
39159
|
-
const { activeElement } = document;
|
|
39160
|
-
if (!activeElement || activeElement === popupEl) {
|
|
39161
|
-
return;
|
|
39162
|
-
}
|
|
39163
|
-
if (!popupEl.contains(activeElement)) {
|
|
39164
|
-
return;
|
|
39165
|
-
}
|
|
39166
|
-
const scrollContainer = getScrollContainer(activeElement);
|
|
39167
|
-
if (!scrollContainer || !popupEl.contains(scrollContainer)) {
|
|
39168
|
-
// What scrolls the field is outside the popup, which means the page: a
|
|
39169
|
-
// popup holds it still on purpose (trapScrollInside), so there is nothing
|
|
39170
|
-
// here to scroll back.
|
|
39171
|
-
return;
|
|
39172
|
-
}
|
|
39173
|
-
const scrollportHeight = scrollContainer.clientHeight;
|
|
39174
|
-
const scrollportHeightBefore = scrollportHeightMap.get(scrollContainer);
|
|
39175
|
-
scrollportHeightMap.set(scrollContainer, scrollportHeight);
|
|
39176
|
-
if (scrollportHeightBefore !== undefined) {
|
|
39177
|
-
if (scrollportHeight >= scrollportHeightBefore) {
|
|
39178
|
-
// Nothing was taken away, so nothing was hidden by this.
|
|
39179
|
-
return;
|
|
39180
|
-
}
|
|
39181
|
-
if (
|
|
39182
|
-
!isVisibleInScrollport(
|
|
39183
|
-
activeElement,
|
|
39184
|
-
scrollContainer,
|
|
39185
|
-
scrollportHeightBefore,
|
|
39186
|
-
)
|
|
39187
|
-
) {
|
|
39188
|
-
return;
|
|
39189
|
-
}
|
|
39190
|
-
}
|
|
39191
|
-
scrollIntoViewScoped(activeElement, {
|
|
39192
|
-
container: scrollContainer,
|
|
39193
|
-
block: "nearest",
|
|
39194
|
-
});
|
|
39195
|
-
};
|
|
39196
|
-
|
|
39197
|
-
// Whether any part of `el` was showing in `container` back when its scrollport
|
|
39198
|
-
// was `scrollportHeight` tall. Both boxes are read now: the container may have
|
|
39199
|
-
// moved as well as shrunk (a centered dialog re-centers itself), and the
|
|
39200
|
-
// difference between the two tops is what that move leaves alone.
|
|
39201
|
-
const isVisibleInScrollport = (el, container, scrollportHeight) => {
|
|
39202
|
-
const elRect = el.getBoundingClientRect();
|
|
39203
|
-
const containerRect = container.getBoundingClientRect();
|
|
39204
|
-
const elTop = elRect.top - containerRect.top + container.scrollTop;
|
|
39205
|
-
const elBottom = elTop + elRect.height;
|
|
39206
|
-
const scrollportTop = container.scrollTop;
|
|
39207
|
-
const scrollportBottom = scrollportTop + scrollportHeight;
|
|
39208
|
-
return elBottom > scrollportTop && elTop < scrollportBottom;
|
|
39209
|
-
};
|
|
39210
|
-
|
|
39211
|
-
/**
|
|
39212
|
-
* Calls `onSettled` once `el`'s current CSS transition is over — via
|
|
39213
|
-
* `transitionend`, with a safety `setTimeout` fallback matching the longest
|
|
39214
|
-
* `transition-duration`, in case nothing actually transitions or an event is
|
|
39215
|
-
* missed.
|
|
39216
|
-
*
|
|
39217
|
-
* Returns a "cancel" function, so a caller whose instance has been superseded
|
|
39218
|
-
* (a fresh open/close about to set its own state) can keep this stale one from
|
|
39219
|
-
* firing later. Cancelling only stops `onSettled`: undoing whatever the caller
|
|
39220
|
-
* did up front is that fresh call's business, not this one's.
|
|
39221
|
-
*/
|
|
39222
|
-
const whenTransitionSettles = (el, onSettled) => {
|
|
39223
|
-
let settled = false;
|
|
39224
|
-
const onTransitionEnd = (transitionEvent) => {
|
|
39225
|
-
if (transitionEvent.target === el) {
|
|
39226
|
-
finish();
|
|
39227
|
-
}
|
|
39228
|
-
};
|
|
39229
|
-
const stopWatching = () => {
|
|
39230
|
-
settled = true;
|
|
39231
|
-
el.removeEventListener("transitionend", onTransitionEnd);
|
|
39232
|
-
clearTimeout(safetyTimeoutId);
|
|
39233
|
-
};
|
|
39234
|
-
const finish = () => {
|
|
39235
|
-
if (settled) {
|
|
39236
|
-
return;
|
|
39237
|
-
}
|
|
39238
|
-
stopWatching();
|
|
39239
|
-
onSettled();
|
|
39240
|
-
};
|
|
39241
|
-
el.addEventListener("transitionend", onTransitionEnd);
|
|
39242
|
-
const durationsInSeconds = getComputedStyle(el)
|
|
39243
|
-
.transitionDuration.split(",")
|
|
39244
|
-
.map((value) => parseFloat(value) || 0);
|
|
39245
|
-
const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
|
|
39246
|
-
const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
|
|
39247
|
-
return () => {
|
|
39248
|
-
if (settled) {
|
|
39249
|
-
return;
|
|
39250
|
-
}
|
|
39251
|
-
stopWatching();
|
|
39252
|
-
};
|
|
39253
|
-
};
|
|
39254
|
-
|
|
39255
|
-
/**
|
|
39256
|
-
* Drops the document's text selection when it lives inside `el`, leaving a
|
|
39257
|
-
* selection made elsewhere on the page alone.
|
|
39258
|
-
*
|
|
39259
|
-
* A popup being closed takes its content with it, and a selection is a claim
|
|
39260
|
-
* on content the user can still act on: kept, it would outlive the surface it
|
|
39261
|
-
* was made on — painted on the box for the length of its exit transition, with
|
|
39262
|
-
* the handles and the copy toolbar a phone draws from the live selection
|
|
39263
|
-
* hanging over something that is going away — and reappear with the box on
|
|
39264
|
-
* the next opening. Dropping the selection is the only way to remove that
|
|
39265
|
-
* chrome: a `user-select: none` on the closing box hides the highlight in some
|
|
39266
|
-
* browsers only, and the handles are the browser's own, drawn from the
|
|
39267
|
-
* selection object rather than from any style.
|
|
39268
|
-
*/
|
|
39269
|
-
const clearTextSelectionInside = (el) => {
|
|
39270
|
-
const selection = window.getSelection();
|
|
39271
|
-
if (!selection || selection.rangeCount === 0) {
|
|
39272
|
-
return;
|
|
39273
|
-
}
|
|
39274
|
-
const range = selection.getRangeAt(0);
|
|
39275
|
-
if (!el.contains(range.commonAncestorContainer)) {
|
|
39276
|
-
return;
|
|
39277
|
-
}
|
|
39278
|
-
selection.removeAllRanges();
|
|
39279
|
-
};
|
|
39280
|
-
|
|
39281
|
-
/**
|
|
39282
|
-
* Disables pointer-events on `el` until its current CSS transition settles —
|
|
39283
|
-
* avoids the cursor changing/something becoming clickable while the popup is
|
|
39284
|
-
* still visually moving into or out of place.
|
|
39285
|
-
*
|
|
39286
|
-
* Returns whenTransitionSettles' own "cancel" function: it doesn't restore
|
|
39287
|
-
* pointer-events, since a fresh call for the next open/close is about to set
|
|
39288
|
-
* its own state.
|
|
39289
|
-
*/
|
|
39290
|
-
const suppressPointerEventsDuringTransition = (el) => {
|
|
39291
|
-
el.style.pointerEvents = "none";
|
|
39292
|
-
return whenTransitionSettles(el, () => {
|
|
39293
|
-
el.style.pointerEvents = "";
|
|
39294
|
-
});
|
|
39295
|
-
};
|
|
39296
|
-
|
|
39297
|
-
/**
|
|
39298
|
-
* Hides the backdrop, deferring until the browser's matching "click" fires
|
|
39299
|
-
* when `closeEvent` was triggered by a mousedown (see popover.jsx's top
|
|
39300
|
-
* comment for why) — same capture-phase-on-document pattern as
|
|
39301
|
-
* armSuppressNextOpenRequest in open_controller.js, which a plain timeout
|
|
39302
|
-
* can't safely replace: mouseup (and the click that follows it) can land an
|
|
39303
|
-
* arbitrarily long time after mousedown (the user is still holding the
|
|
39304
|
-
* button down), so a short timeout can fire first and hide the backdrop
|
|
39305
|
-
* before its own click ever arrives. A capture-phase listener on document
|
|
39306
|
-
* fires for every click regardless of what any bubble-phase handler does
|
|
39307
|
-
* downstream, so no fallback timer is needed.
|
|
39308
|
-
*
|
|
39309
|
-
* `hide` is the caller's own way to actually hide the backdrop
|
|
39310
|
-
* (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
|
|
39311
|
-
* for a plain div) — this helper only owns the mousedown/click timing.
|
|
39312
|
-
*
|
|
39313
|
-
* Returns a disarm function (or undefined if hidden immediately), so a
|
|
39314
|
-
* fresh open can cancel a pending hide it's about to make redundant.
|
|
39315
|
-
*/
|
|
39316
|
-
const armPointerDownOutsideClose = (closeEvent, hide) => {
|
|
39317
|
-
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
39318
|
-
if (!mousedownEvent) {
|
|
39319
|
-
hide();
|
|
39320
|
-
return undefined;
|
|
39321
|
-
}
|
|
39322
|
-
const onClick = () => {
|
|
39323
|
-
document.removeEventListener("click", onClick, { capture: true });
|
|
39324
|
-
hide();
|
|
39325
|
-
};
|
|
39326
|
-
document.addEventListener("click", onClick, { capture: true });
|
|
39327
|
-
return () => {
|
|
39328
|
-
document.removeEventListener("click", onClick, { capture: true });
|
|
39329
|
-
};
|
|
39330
|
-
};
|
|
39331
|
-
|
|
39332
|
-
/**
|
|
39333
|
-
* Maps a positionArea y/x pair to a concrete `navi-animation` value (a
|
|
39334
|
-
* `prefix` plus a direction word), or `null` if both axes overlap the anchor
|
|
39335
|
-
* (no direction at all — that's `resolvedAnimationKind === "scaling"`
|
|
39336
|
-
* territory instead, see resolveAutoAnimationKind below).
|
|
39337
|
-
*
|
|
39338
|
-
* `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
|
|
39339
|
-
* when docked) keeps the word as the compass direction the popup comes
|
|
39340
|
-
* from: placed "top" (a point/corner), it slides in from the top.
|
|
39341
|
-
* `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
|
|
39342
|
-
* direction instead, the opposite compass point: placed "top" of the
|
|
39343
|
-
* anchor, it moves/grows up, away from the anchor (which sits below it).
|
|
39344
|
-
*
|
|
39345
|
-
* "inset-*"/"center" contribute no direction on their axis either way.
|
|
39346
|
-
*/
|
|
39347
|
-
const resolveDirectionValue = (y, x, { prefix }) => {
|
|
39348
|
-
const yWord =
|
|
39349
|
-
y === "top"
|
|
39350
|
-
? prefix === "expand"
|
|
39351
|
-
? "up"
|
|
39352
|
-
: "top"
|
|
39353
|
-
: y === "bottom"
|
|
39354
|
-
? prefix === "expand"
|
|
39355
|
-
? "down"
|
|
39356
|
-
: "bottom"
|
|
39357
|
-
: null;
|
|
39358
|
-
const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
|
|
39359
|
-
if (!yWord && !xWord) {
|
|
39360
|
-
return null;
|
|
39361
|
-
}
|
|
39362
|
-
return yWord && xWord
|
|
39363
|
-
? `${prefix}-${yWord}-${xWord}`
|
|
39364
|
-
: `${prefix}-${yWord || xWord}`;
|
|
39365
|
-
};
|
|
39366
|
-
|
|
39367
|
-
/**
|
|
39368
|
-
* Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
|
|
39369
|
-
* — picked for any real anchor, or for a point/corner placed dead-center
|
|
39370
|
-
* (both positionArea axes overlapping — there's no sensible direction to
|
|
39371
|
-
* slide from in that case). "sliding" otherwise. `anchor` is `undefined`
|
|
39372
|
-
* for any no-anchor/docked case (Dialog always, Popover's own custom
|
|
39373
|
-
* renderer when there's no real anchor), so this collapses to "scaling"
|
|
39374
|
-
* there only for the dead-center case, "sliding" otherwise. The two
|
|
39375
|
-
* "overlapping" booleans below describe the *positionArea* itself (a bare
|
|
39376
|
-
* word vs. "inset-"/"center"), not anything about the anchor — they'd
|
|
39377
|
-
* mean exactly the same thing even with no anchor at all, since it's the
|
|
39378
|
-
* position strategy, not the anchor, that decides whether there's a
|
|
39379
|
-
* direction to slide from.
|
|
39380
|
-
*/
|
|
39381
|
-
const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
|
|
39382
|
-
const yIsOverlapping =
|
|
39383
|
-
parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
|
|
39384
|
-
const xIsOverlapping =
|
|
39385
|
-
parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
|
|
39386
|
-
return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
|
|
39387
|
-
};
|
|
39388
|
-
|
|
39389
|
-
/**
|
|
39390
|
-
* An opening that reaches a popup with no element of its own to show.
|
|
39391
|
-
*
|
|
39392
|
-
* The one cause seen so far, and it is silent: something inside the popup
|
|
39393
|
-
* suspended while its content was being built, and the `<Loading>` that caught
|
|
39394
|
-
* it sits ABOVE the popup. A boundary suspending on an update keeps the page as
|
|
39395
|
-
* it was, as a copy, so what is opened here belongs to a copy nobody owns any
|
|
39396
|
-
* more — and once the data arrives the subtree is rebuilt from scratch, closed
|
|
39397
|
-
* and empty. Nothing is thrown, nothing is drawn, and the press is lost.
|
|
39398
|
-
*/
|
|
39399
|
-
const warnPopupHasNoElementToOpen = (popupKind) => {
|
|
39400
|
-
console.warn(
|
|
39401
|
-
`[navi] a "${popupKind}" was asked to open and has no element to open. What usually did it: content inside it suspended, and the <Loading> that caught the wait is above the popup rather than inside it — the popup was set aside with the rest of what that boundary holds, and this opening is lost (the next one works). Put a <Loading> inside the popup, or draw the wait in the component itself with useAsyncData(action, { loading: true }).`,
|
|
39402
|
-
);
|
|
39403
|
-
};
|
|
39404
|
-
|
|
39405
|
-
/**
|
|
39406
|
-
* A popup reads "outside" from its own border box: what a press lands on
|
|
39407
|
-
* decides nothing on its own (a genuine backdrop press and a press on the
|
|
39408
|
-
* popup's padding both report the popup element as their target, there being
|
|
39409
|
-
* no real ::backdrop node to be one), so the rectangle is what tells them
|
|
39410
|
-
* apart. That holds as long as the box and what the popup paints are the same
|
|
39411
|
-
* thing — which stops being true for a popup with no surface of its own
|
|
39412
|
-
* (`backgroundColor="transparent"`, no shadow, no padding): what the eye reads
|
|
39413
|
-
* as backdrop is then inside the box, and a press there stays a press on the
|
|
39414
|
-
* popup.
|
|
39415
|
-
*
|
|
39416
|
-
* `data-navi-popup-outside` is how a caller says which of its own boxes are
|
|
39417
|
-
* not the surface. It is opt-in because navi cannot infer it — a background
|
|
39418
|
-
* can come from anywhere — while the caller who made the popup see-through
|
|
39419
|
-
* knows exactly which box is decoration and which is paper.
|
|
39420
|
-
*
|
|
39421
|
-
* The marker answers for the element it is on, never for its descendants: a
|
|
39422
|
-
* box painted inside a marked one is still surface, so a row can be marked and
|
|
39423
|
-
* its empty halves read as backdrop while presses on the controls it holds do
|
|
39424
|
-
* not dismiss anything. A descendant with `pointer-events: none` never becomes
|
|
39425
|
-
* a press target at all, so the marked box answers in its place — while the
|
|
39426
|
-
* same declaration on the marked box itself takes it out of hit-testing and
|
|
39427
|
-
* makes the marker unreachable (see warnAboutUnreachableOutsideRegions).
|
|
39428
|
-
*/
|
|
39429
|
-
const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
|
|
39430
|
-
|
|
39431
|
-
/**
|
|
39432
|
-
* `data-navi-popup-inside` is the other direction: a box of the PAGE the
|
|
39433
|
-
* caller says belongs to a popup, so a press on it is not outside that popup
|
|
39434
|
-
* even though it lies beyond the border box — a card whose press is what
|
|
39435
|
-
* fills the one panel of a board. Its value names the popup by `id`, the way
|
|
39436
|
-
* `commandfor` does (several ids, space separated), because the exemption is
|
|
39437
|
-
* one popup's: a menu open beside the board still closes on the same press.
|
|
39438
|
-
*
|
|
39439
|
-
* Unlike the outside marker it answers for its whole subtree: the outside
|
|
39440
|
-
* marker carves free space out of a surface whose controls stay surface, this
|
|
39441
|
-
* one adds a whole thing — the card, its count, its buttons — to the popup's
|
|
39442
|
-
* own ground, and nothing in it is a dismissal.
|
|
39443
|
-
*/
|
|
39444
|
-
const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
|
|
39445
|
-
|
|
39662
|
+
return undefined;
|
|
39663
|
+
}
|
|
39664
|
+
return {
|
|
39665
|
+
target,
|
|
39666
|
+
implementation: () => {
|
|
39667
|
+
const isExpanded = target.getAttribute("aria-expanded") === "true";
|
|
39668
|
+
const customEventName = isExpanded
|
|
39669
|
+
? "navi_request_close"
|
|
39670
|
+
: "navi_request_open";
|
|
39671
|
+
return dispatchCustomEvent(target, customEventName, {
|
|
39672
|
+
event,
|
|
39673
|
+
source: resolveCommandProxySource(source),
|
|
39674
|
+
anchor,
|
|
39675
|
+
// Same as --navi-open below: the half of the toggle that opens says
|
|
39676
|
+
// what it opens ON, and the half that closes carries it too so the
|
|
39677
|
+
// popup never has to ask which half it just heard.
|
|
39678
|
+
value:
|
|
39679
|
+
value === undefined ? resolveCommandValue(source, event) : value,
|
|
39680
|
+
});
|
|
39681
|
+
},
|
|
39682
|
+
};
|
|
39683
|
+
},
|
|
39684
|
+
);
|
|
39685
|
+
// A popup that edits opens ON something, and the press is the only place that
|
|
39686
|
+
// knows which one. It says it with `value` — "what this is about", as
|
|
39687
|
+
// everywhere else — rather than with an argument, which says what the command
|
|
39688
|
+
// DOES: "open" is already a complete instruction, unlike --navi-go-to-slide.
|
|
39689
|
+
// <Button value={radar.id} command="--navi-open" commandfor="radar-dialog">
|
|
39690
|
+
// The popup is told before it opens — see Dialog/Popover's `onOpen`.
|
|
39691
|
+
registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
|
|
39692
|
+
const target =
|
|
39693
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
39694
|
+
if (!target) {
|
|
39695
|
+
return undefined;
|
|
39696
|
+
}
|
|
39697
|
+
return {
|
|
39698
|
+
target,
|
|
39699
|
+
implementation: () => {
|
|
39700
|
+
// The popup reads `anchor` first and falls back to the source (see
|
|
39701
|
+
// onnavi_request_open in popover.jsx): without an `anchor`, the element
|
|
39702
|
+
// asking is the element opened against.
|
|
39703
|
+
return dispatchCustomEvent(target, "navi_request_open", {
|
|
39704
|
+
event,
|
|
39705
|
+
source: resolveCommandProxySource(source),
|
|
39706
|
+
anchor,
|
|
39707
|
+
value: value === undefined ? resolveCommandValue(source, event) : value,
|
|
39708
|
+
});
|
|
39709
|
+
},
|
|
39710
|
+
};
|
|
39711
|
+
});
|
|
39446
39712
|
/**
|
|
39447
|
-
*
|
|
39448
|
-
*
|
|
39713
|
+
* Tells the popup a source opens that a press on it has started, ahead of the
|
|
39714
|
+
* release that runs the command: what the opening will read can be asked for
|
|
39715
|
+
* now (see open_controller.js). A press rather than a hover, because what
|
|
39716
|
+
* that reads is keyed by the popup — one sheet per card — and a pointer
|
|
39717
|
+
* crossing a list of cards has chosen none of them; on a touch screen the
|
|
39718
|
+
* press is the first thing the finger says anyway.
|
|
39449
39719
|
*
|
|
39450
|
-
*
|
|
39451
|
-
*
|
|
39452
|
-
* (never an ancestor), so a press inside the popup's box never reaches it.
|
|
39720
|
+
* The target is found the way --navi-open finds it, but silently: nothing
|
|
39721
|
+
* runs here, and a source with no target says so at the release.
|
|
39453
39722
|
*/
|
|
39454
|
-
const
|
|
39455
|
-
|
|
39456
|
-
{ popupEl, openController, pressOutside },
|
|
39457
|
-
) => {
|
|
39458
|
-
if (mouseDownEvent.button !== 0) {
|
|
39723
|
+
const announceOpeningPress = (source, command) => {
|
|
39724
|
+
if (command !== "--navi-open" && command !== "--navi-toggle") {
|
|
39459
39725
|
return;
|
|
39460
39726
|
}
|
|
39461
|
-
const
|
|
39462
|
-
|
|
39727
|
+
const commandFor = source.getAttribute("commandfor");
|
|
39728
|
+
const target = commandFor
|
|
39729
|
+
? document.getElementById(commandFor)
|
|
39730
|
+
: resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
39731
|
+
if (!target) {
|
|
39463
39732
|
return;
|
|
39464
39733
|
}
|
|
39465
|
-
|
|
39466
|
-
|
|
39467
|
-
|
|
39468
|
-
|
|
39734
|
+
dispatchCustomEvent(target, "navi_open_press");
|
|
39735
|
+
};
|
|
39736
|
+
// "--navi-close:all" closes every expandable above the source, nearest first —
|
|
39737
|
+
// a link leaving from a badge shown over a sheet leaves both. A surface that
|
|
39738
|
+
// refuses (a form asking about its changes) keeps what is above it open too:
|
|
39739
|
+
// one cannot be out of the sheet while still in the badge.
|
|
39740
|
+
registerNaviCommand("--navi-close", (source, event, { argument }) => {
|
|
39741
|
+
const target =
|
|
39742
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
39743
|
+
if (!target) {
|
|
39744
|
+
return undefined;
|
|
39469
39745
|
}
|
|
39470
|
-
|
|
39471
|
-
|
|
39472
|
-
|
|
39473
|
-
|
|
39474
|
-
|
|
39475
|
-
|
|
39476
|
-
|
|
39477
|
-
|
|
39746
|
+
return {
|
|
39747
|
+
target,
|
|
39748
|
+
implementation: () => {
|
|
39749
|
+
const detail = { event, source: resolveCommandProxySource(source) };
|
|
39750
|
+
if (argument === "all") {
|
|
39751
|
+
return requestCloseUpward(target, detail);
|
|
39752
|
+
}
|
|
39753
|
+
return dispatchCustomEvent(target, "navi_request_close", detail);
|
|
39754
|
+
},
|
|
39755
|
+
};
|
|
39756
|
+
});
|
|
39757
|
+
const requestCloseUpward = (target, detail) => {
|
|
39758
|
+
let expandable = target;
|
|
39759
|
+
while (expandable) {
|
|
39760
|
+
const closing = dispatchCustomEvent(
|
|
39761
|
+
expandable,
|
|
39762
|
+
"navi_request_close",
|
|
39763
|
+
detail,
|
|
39764
|
+
);
|
|
39765
|
+
if (!closing) {
|
|
39766
|
+
return false;
|
|
39767
|
+
}
|
|
39768
|
+
const parent = expandable.parentElement;
|
|
39769
|
+
expandable = parent ? parent.closest("[aria-expanded]") : null;
|
|
39478
39770
|
}
|
|
39479
|
-
|
|
39480
|
-
|
|
39481
|
-
|
|
39771
|
+
return true;
|
|
39772
|
+
};
|
|
39773
|
+
registerNaviCommand("--navi-cancel", (source, event) => {
|
|
39774
|
+
const target =
|
|
39775
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
39776
|
+
if (!target) {
|
|
39777
|
+
return undefined;
|
|
39482
39778
|
}
|
|
39483
|
-
|
|
39484
|
-
|
|
39485
|
-
|
|
39486
|
-
|
|
39779
|
+
return {
|
|
39780
|
+
target,
|
|
39781
|
+
implementation: () => {
|
|
39782
|
+
return dispatchCustomEvent(target, "navi_request_close", {
|
|
39783
|
+
event,
|
|
39784
|
+
source: resolveCommandProxySource(source),
|
|
39785
|
+
isCancel: true,
|
|
39786
|
+
});
|
|
39787
|
+
},
|
|
39788
|
+
};
|
|
39789
|
+
});
|
|
39790
|
+
// "Yes, do it" — said inside the popup of a confirm picker (see
|
|
39791
|
+
// picker/picker_confirm.jsx), by its own yes button or by whatever a caller put
|
|
39792
|
+
// there instead. Its counterpart is --navi-cancel; the difference between the
|
|
39793
|
+
// two is this event, since both then close the popup, and a popup that closes
|
|
39794
|
+
// without having said this is the answer "no".
|
|
39795
|
+
registerNaviCommand("--navi-confirm", (source, event) => {
|
|
39796
|
+
const target =
|
|
39797
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
39798
|
+
if (!target) {
|
|
39799
|
+
return undefined;
|
|
39800
|
+
}
|
|
39801
|
+
return {
|
|
39802
|
+
target,
|
|
39803
|
+
implementation: () => {
|
|
39804
|
+
const commandSource = resolveCommandProxySource(source);
|
|
39805
|
+
dispatchCustomEvent(target, "navi_request_confirm", {
|
|
39806
|
+
event,
|
|
39807
|
+
source: commandSource,
|
|
39808
|
+
});
|
|
39809
|
+
return dispatchCustomEvent(target, "navi_request_close", {
|
|
39810
|
+
event,
|
|
39811
|
+
source: commandSource,
|
|
39812
|
+
});
|
|
39813
|
+
},
|
|
39814
|
+
};
|
|
39815
|
+
});
|
|
39816
|
+
registerNaviCommand("--navi-define", (source, event) => {
|
|
39817
|
+
const target =
|
|
39818
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
39819
|
+
if (!target) {
|
|
39820
|
+
return undefined;
|
|
39487
39821
|
}
|
|
39822
|
+
return {
|
|
39823
|
+
target,
|
|
39824
|
+
implementation: () => executeNaviDefine(source, event, target),
|
|
39825
|
+
};
|
|
39826
|
+
});
|
|
39827
|
+
// A send whose target is the popup itself: what is around the source IS the
|
|
39828
|
+
// popup's answer, and a picker runs its action with that answer as it closes
|
|
39829
|
+
// (see onRequestClose in picker_custom.jsx) — dispatching the action here too
|
|
39830
|
+
// would run it twice. `isSend` tells this close from a dismissal (the cross,
|
|
39831
|
+
// a press outside, Escape): the person pressed a choice, which a picker may
|
|
39832
|
+
// act on even when the choice is the one it already held.
|
|
39833
|
+
const executeNaviDefine = (source, event, target) => {
|
|
39834
|
+
return dispatchCustomEvent(target, "navi_request_close", {
|
|
39835
|
+
event,
|
|
39836
|
+
source: resolveCommandProxySource(source),
|
|
39837
|
+
isSend: true,
|
|
39838
|
+
});
|
|
39488
39839
|
};
|
|
39489
39840
|
|
|
39490
|
-
|
|
39491
|
-
|
|
39492
|
-
|
|
39493
|
-
|
|
39494
|
-
|
|
39495
|
-
* an ancestor that is still the popup's own surface. Writing them next to the
|
|
39496
|
-
* marker is the natural mistake, and it makes the marker silently unreachable
|
|
39497
|
-
* — so it is said out loud, once per open.
|
|
39498
|
-
*/
|
|
39499
|
-
const warnAboutUnreachableOutsideRegions = (popupEl) => {
|
|
39500
|
-
for (const regionEl of popupEl.querySelectorAll(
|
|
39501
|
-
`[${OUTSIDE_REGION_ATTRIBUTE}]`,
|
|
39502
|
-
)) {
|
|
39503
|
-
if (getComputedStyle(regionEl).pointerEvents === "none") {
|
|
39504
|
-
console.warn(
|
|
39505
|
-
`[navi] ${OUTSIDE_REGION_ATTRIBUTE} on an element with "pointer-events: none" is never read: the press is answered by the nearest ancestor still in hit-testing, which is the popup itself. Drop pointer-events; the marker alone is what makes a press there count as outside.`,
|
|
39506
|
-
regionEl,
|
|
39507
|
-
);
|
|
39508
|
-
}
|
|
39841
|
+
registerNaviCommand("--navi-scroll", (source, event) => {
|
|
39842
|
+
const target =
|
|
39843
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39844
|
+
if (!target) {
|
|
39845
|
+
return undefined;
|
|
39509
39846
|
}
|
|
39510
|
-
|
|
39847
|
+
return {
|
|
39848
|
+
target,
|
|
39849
|
+
implementation: () => {
|
|
39850
|
+
return dispatchCustomEvent(target, "navi_request_scroll", {
|
|
39851
|
+
event,
|
|
39852
|
+
id: resolveCommandValue(source, event),
|
|
39853
|
+
});
|
|
39854
|
+
},
|
|
39855
|
+
};
|
|
39856
|
+
});
|
|
39857
|
+
// The press a hand is still making, given to something that can be carried: a
|
|
39858
|
+
// hold opened a popup, and what the popup holds is to follow that same finger
|
|
39859
|
+
// without it lifting and pressing again. `event` is what the press set off (the
|
|
39860
|
+
// popup's open event), and the press is read from its chain. Run once the popup
|
|
39861
|
+
// has settled, which the caller cannot see: the carry measures its element and
|
|
39862
|
+
// puts a copy in the top layer, neither of which holds while a transition is
|
|
39863
|
+
// still moving the box (see afterPopupLift). What the carry then is — its
|
|
39864
|
+
// outcomes, its refusal — is the element's (see interaction_drag.js).
|
|
39865
|
+
registerNaviCommand("--navi-grab", (source, event) => {
|
|
39866
|
+
const target =
|
|
39867
|
+
resolveExplicitTarget(source) || source.closest("[data-drag-source]");
|
|
39868
|
+
if (!target) {
|
|
39869
|
+
return undefined;
|
|
39870
|
+
}
|
|
39871
|
+
return {
|
|
39872
|
+
target,
|
|
39873
|
+
implementation: () => {
|
|
39874
|
+
afterPopupLift(() => {
|
|
39875
|
+
// Taken away with the popup before it settled.
|
|
39876
|
+
if (!target.isConnected) {
|
|
39877
|
+
return;
|
|
39878
|
+
}
|
|
39879
|
+
dispatchCustomEvent(target, "navi_request_grab", { event });
|
|
39880
|
+
});
|
|
39881
|
+
return true;
|
|
39882
|
+
},
|
|
39883
|
+
};
|
|
39884
|
+
});
|
|
39885
|
+
registerNaviCommand("--navi-check", (source, event) => {
|
|
39886
|
+
const target =
|
|
39887
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39888
|
+
if (!target) {
|
|
39889
|
+
return undefined;
|
|
39890
|
+
}
|
|
39891
|
+
return {
|
|
39892
|
+
target,
|
|
39893
|
+
implementation: () => {
|
|
39894
|
+
return dispatchCustomEvent(target, "navi_request_check", {
|
|
39895
|
+
event,
|
|
39896
|
+
});
|
|
39897
|
+
},
|
|
39898
|
+
};
|
|
39899
|
+
});
|
|
39900
|
+
registerNaviCommand("--navi-uncheck", (source, event) => {
|
|
39901
|
+
const target =
|
|
39902
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39903
|
+
if (!target) {
|
|
39904
|
+
return undefined;
|
|
39905
|
+
}
|
|
39906
|
+
return {
|
|
39907
|
+
target,
|
|
39908
|
+
implementation: () => {
|
|
39909
|
+
return dispatchCustomEvent(target, "navi_request_uncheck", {
|
|
39910
|
+
event,
|
|
39911
|
+
});
|
|
39912
|
+
},
|
|
39913
|
+
};
|
|
39914
|
+
});
|
|
39915
|
+
registerNaviCommand("--navi-select", (source, event) => {
|
|
39916
|
+
const target =
|
|
39917
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39918
|
+
if (!target) {
|
|
39919
|
+
return undefined;
|
|
39920
|
+
}
|
|
39921
|
+
return {
|
|
39922
|
+
target,
|
|
39923
|
+
implementation: () => {
|
|
39924
|
+
return dispatchCustomEvent(target, "navi_request_select", {
|
|
39925
|
+
event,
|
|
39926
|
+
id: resolveCommandValue(source, event),
|
|
39927
|
+
});
|
|
39928
|
+
},
|
|
39929
|
+
};
|
|
39930
|
+
});
|
|
39931
|
+
registerNaviCommand("--navi-unselect", (source, event) => {
|
|
39932
|
+
const target =
|
|
39933
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39934
|
+
if (!target) {
|
|
39935
|
+
return undefined;
|
|
39936
|
+
}
|
|
39937
|
+
return {
|
|
39938
|
+
target,
|
|
39939
|
+
implementation: () => {
|
|
39940
|
+
return dispatchCustomEvent(target, "navi_request_unselect", {
|
|
39941
|
+
event,
|
|
39942
|
+
id: resolveCommandValue(source, event),
|
|
39943
|
+
});
|
|
39944
|
+
},
|
|
39945
|
+
};
|
|
39946
|
+
});
|
|
39511
39947
|
|
|
39512
39948
|
/**
|
|
39513
|
-
*
|
|
39514
|
-
*
|
|
39515
|
-
*
|
|
39516
|
-
* caller asked to leave the page reachable (`backdrop={false}`).
|
|
39517
|
-
*
|
|
39518
|
-
* The press is read and never taken: no `preventDefault`, no
|
|
39519
|
-
* `stopPropagation`, and the listener is on `document` rather than on
|
|
39520
|
-
* anything covering the page — so what the press landed on hears it too, in
|
|
39521
|
-
* the same gesture, and the popup only takes note. That is the whole point of
|
|
39522
|
-
* a popup with no wall: a press outside is a press on the page, and the page
|
|
39523
|
-
* answers it. The one popup where that cannot hold is the modal dialog, where
|
|
39524
|
-
* the browser has already made everything behind inert before this runs.
|
|
39525
|
-
*
|
|
39526
|
-
* `pressEventType` says which event IS the press, and the two callers want
|
|
39527
|
-
* opposite ones:
|
|
39528
|
-
*
|
|
39529
|
-
* - `"pointerdown"` for a popup with no wall: a page with no wall over it is
|
|
39530
|
-
* a page whose own elements arbitrate their presses, and cancelling a
|
|
39531
|
-
* `pointerdown` — what a drag source and a control keeping the focus where
|
|
39532
|
-
* it is both do — suppresses every compatibility mouse event that would have
|
|
39533
|
-
* followed it. The press happens, reaches its target, and no `mousedown` is
|
|
39534
|
-
* ever dispatched for it. Reading the pointer event is reading the press
|
|
39535
|
-
* itself, whatever anything does with it afterwards, and it is the same
|
|
39536
|
-
* event the drag and surface detectors read.
|
|
39537
|
-
* - `"mousedown"` for the modal dialog, whose native `::backdrop` exists to
|
|
39538
|
-
* spend the press. The page's controls act on `mousedown`, and on a touch
|
|
39539
|
-
* screen that is not the same moment as `pointerdown`: the compatibility
|
|
39540
|
-
* mouse events are synthesized at `touchend` and hit-tested again then.
|
|
39541
|
-
* Closing on `pointerdown` takes the `::backdrop` away before that second
|
|
39542
|
-
* hit-test, so the `mousedown` lands on whatever the page holds under the
|
|
39543
|
-
* finger, and one tap both dismisses the dialog and presses the page. Read
|
|
39544
|
-
* on `mousedown`, the wall is still up when the press the page acts on is
|
|
39545
|
-
* aimed, and it catches it. Nothing behind a modal wall can cancel that
|
|
39546
|
-
* press, since nothing behind it hears the `pointerdown` at all.
|
|
39949
|
+
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
39950
|
+
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
39951
|
+
* mounted run inside the call too, exactly as they would on any other commit.
|
|
39547
39952
|
*
|
|
39548
|
-
*
|
|
39549
|
-
*
|
|
39953
|
+
* For the caller that has to read the DOM it just asked for — measuring an
|
|
39954
|
+
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
39955
|
+
* to do it, because what comes after is a browser event still in flight
|
|
39956
|
+
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
39550
39957
|
*
|
|
39551
|
-
*
|
|
39958
|
+
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
39959
|
+
* render queue drains; swapping it for "right now" for the duration of the call
|
|
39960
|
+
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
39961
|
+
* case above: rendering synchronously in the middle of an event gives up the
|
|
39962
|
+
* batching that makes several state changes one commit.
|
|
39552
39963
|
*/
|
|
39553
|
-
const
|
|
39554
|
-
|
|
39555
|
-
|
|
39556
|
-
)
|
|
39557
|
-
const onDocumentPointerDown = (pointerDownEvent) => {
|
|
39558
|
-
if (pointerDownEvent.button !== 0) {
|
|
39559
|
-
return;
|
|
39560
|
-
}
|
|
39561
|
-
if (openedDuringThisPress(openController)) {
|
|
39562
|
-
// The release of the press that opened it is not somebody dismissing it
|
|
39563
|
-
// (see openedDuringThisPress).
|
|
39564
|
-
return;
|
|
39565
|
-
}
|
|
39566
|
-
// The press landed inside another popup: that is a press on what is in
|
|
39567
|
-
// front, not outside. Asking the target where it lives rather than asking
|
|
39568
|
-
// this popup whether it was pushed — a popup in front does not have to be
|
|
39569
|
-
// one this popup knows about. In front means neither inside nor around: a
|
|
39570
|
-
// popup nested in this one falls to the containment check below as the
|
|
39571
|
-
// inside press it is, and one this popup opened INSIDE (a bubble rendered
|
|
39572
|
-
// in a sheet's content) is around it, so what that popup holds is outside
|
|
39573
|
-
// this one exactly like the rest of the page.
|
|
39574
|
-
const popupUnderPointer = pointerDownEvent.target.closest?.(
|
|
39575
|
-
`[navi-control="dialog"], [navi-control="popover"]`,
|
|
39576
|
-
);
|
|
39577
|
-
if (
|
|
39578
|
-
popupUnderPointer &&
|
|
39579
|
-
popupUnderPointer !== popupEl &&
|
|
39580
|
-
!popupEl.contains(popupUnderPointer) &&
|
|
39581
|
-
!popupUnderPointer.contains(popupEl)
|
|
39582
|
-
) {
|
|
39583
|
-
return;
|
|
39584
|
-
}
|
|
39585
|
-
if (
|
|
39586
|
-
popupEl.id &&
|
|
39587
|
-
pointerDownEvent.target.closest?.(
|
|
39588
|
-
`[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupEl.id)}"]`,
|
|
39589
|
-
)
|
|
39590
|
-
) {
|
|
39591
|
-
return;
|
|
39592
|
-
}
|
|
39593
|
-
// Real DOM containment wins over the rectangle below — an element
|
|
39594
|
-
// genuinely inside the popup (`overflow: visible`, a negative margin, an
|
|
39595
|
-
// absolutely-positioned child) can be painted outside its border box, and
|
|
39596
|
-
// a press there is not outside just because its coordinates are. The
|
|
39597
|
-
// popup element itself is excluded (contains() answers true for it): a
|
|
39598
|
-
// press on a modal dialog's native backdrop reports the dialog as its
|
|
39599
|
-
// target, there being no `::backdrop` node to be one, so treating that as
|
|
39600
|
-
// contained would keep the rectangle from ever running.
|
|
39601
|
-
if (
|
|
39602
|
-
pointerDownEvent.target !== popupEl &&
|
|
39603
|
-
popupEl.contains(pointerDownEvent.target)
|
|
39604
|
-
) {
|
|
39605
|
-
return;
|
|
39606
|
-
}
|
|
39607
|
-
const rect = popupEl.getBoundingClientRect();
|
|
39608
|
-
const isOutside =
|
|
39609
|
-
pointerDownEvent.clientX < rect.left ||
|
|
39610
|
-
pointerDownEvent.clientX > rect.right ||
|
|
39611
|
-
pointerDownEvent.clientY < rect.top ||
|
|
39612
|
-
pointerDownEvent.clientY > rect.bottom;
|
|
39613
|
-
if (!isOutside) {
|
|
39614
|
-
return;
|
|
39615
|
-
}
|
|
39616
|
-
openController.requestClose(pointerDownEvent, {
|
|
39617
|
-
isCancel: pressOutside === "cancel",
|
|
39618
|
-
});
|
|
39619
|
-
};
|
|
39620
|
-
document.addEventListener(pressEventType, onDocumentPointerDown, {
|
|
39621
|
-
capture: true,
|
|
39622
|
-
});
|
|
39623
|
-
return () => {
|
|
39624
|
-
document.removeEventListener(pressEventType, onDocumentPointerDown, {
|
|
39625
|
-
capture: true,
|
|
39626
|
-
});
|
|
39964
|
+
const flushSyncRendering = (fn) => {
|
|
39965
|
+
const debounceRenderingPrevious = options.debounceRendering;
|
|
39966
|
+
options.debounceRendering = (drainRenderQueue) => {
|
|
39967
|
+
drainRenderQueue();
|
|
39627
39968
|
};
|
|
39969
|
+
try {
|
|
39970
|
+
fn();
|
|
39971
|
+
} finally {
|
|
39972
|
+
options.debounceRendering = debounceRenderingPrevious;
|
|
39973
|
+
}
|
|
39628
39974
|
};
|
|
39629
39975
|
|
|
39630
39976
|
/**
|
|
@@ -66937,6 +67283,13 @@ const css$D = /* css */`
|
|
|
66937
67283
|
* is a `Popover`-level answer only for `"close"`/`"cancel"`; `"capture"` has
|
|
66938
67284
|
* nothing left to absorb without a wall, and `"ignore"` renders no wall anyway.
|
|
66939
67285
|
* See docs/popup_backdrop.md.
|
|
67286
|
+
* @param {string} [props.group] - The popover's peers: the triggers carrying
|
|
67287
|
+
* `data-navi-popup-group` with this name. A press on one, while the popover
|
|
67288
|
+
* is open, closes it and opens the peer's popup in that same press — a
|
|
67289
|
+
* menu bar, where pressing another menu switches to it. The wall still
|
|
67290
|
+
* takes the press (the page behind stays out of reach, hover included) and
|
|
67291
|
+
* hands its click to the peer it covered. Without a wall there is nothing
|
|
67292
|
+
* to ask: the press reaches the peer on its own.
|
|
66940
67293
|
* @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="ignore"]
|
|
66941
67294
|
* - `"ignore"` (default): no backdrop at all, outside presses pass straight
|
|
66942
67295
|
* through. `"close"` closes the popover on an outside press, `"cancel"`
|
|
@@ -67313,6 +67666,7 @@ const usePopoverProps = props => {
|
|
|
67313
67666
|
// which is a sibling element (see this file's top comment).
|
|
67314
67667
|
backdropColor,
|
|
67315
67668
|
backdropFilter,
|
|
67669
|
+
group,
|
|
67316
67670
|
scrollCapture,
|
|
67317
67671
|
focusCapture,
|
|
67318
67672
|
// "auto" (default) → the popover follows its content. "frozen" → measured
|
|
@@ -67965,13 +68319,21 @@ const usePopoverProps = props => {
|
|
|
67965
68319
|
}
|
|
67966
68320
|
if (backdropEl) {
|
|
67967
68321
|
backdropEl.setAttribute("aria-expanded", "false");
|
|
67968
|
-
disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent,
|
|
68322
|
+
disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent, clickEvent => {
|
|
67969
68323
|
// Set regardless of isTopLayer — see openEffect's own identical
|
|
67970
68324
|
// comment for why hidePopover() alone isn't reliably sufficient.
|
|
67971
68325
|
backdropEl.setAttribute("navi-hidden", "");
|
|
67972
68326
|
if (isTopLayer) {
|
|
67973
68327
|
backdropEl.hidePopover();
|
|
67974
68328
|
}
|
|
68329
|
+
if (group && clickEvent) {
|
|
68330
|
+
handWallClickToPeer(clickEvent, {
|
|
68331
|
+
wallEl: backdropEl,
|
|
68332
|
+
popupEl: popoverEl,
|
|
68333
|
+
anchorEl: anchorElement,
|
|
68334
|
+
group
|
|
68335
|
+
});
|
|
68336
|
+
}
|
|
67975
68337
|
});
|
|
67976
68338
|
}
|
|
67977
68339
|
restoreFocus?.(closeEvent);
|
|
@@ -68393,6 +68755,10 @@ const Popup = props => {
|
|
|
68393
68755
|
scrollCapture,
|
|
68394
68756
|
positionAreaFixed,
|
|
68395
68757
|
positionAreaWhenAnchorIsInvalid,
|
|
68758
|
+
// A top-layer dialog's wall is its native ::backdrop, which goes with the
|
|
68759
|
+
// dialog on the press that closes it: no click is left on it to hand to a
|
|
68760
|
+
// peer (see handWallClickToPeer).
|
|
68761
|
+
group,
|
|
68396
68762
|
// Dialog-only, destructured out for the same reason: forwarded below only
|
|
68397
68763
|
// in the dialog branch, so they never reach the popover element.
|
|
68398
68764
|
dockedOnSmallTouchScreen,
|
|
@@ -68442,6 +68808,7 @@ const Popup = props => {
|
|
|
68442
68808
|
positionAreaWhenAnchorIsInvalid: positionAreaWhenAnchorIsInvalid,
|
|
68443
68809
|
scrollCapture: scrollCapture === "popover" || scrollCapture,
|
|
68444
68810
|
positionAreaFixed: positionAreaFixed,
|
|
68811
|
+
group: group,
|
|
68445
68812
|
className: withPropsClassName("navi_popup", className),
|
|
68446
68813
|
expand: expand,
|
|
68447
68814
|
expandX: expandX,
|
|
@@ -69359,13 +69726,18 @@ const PickerContentInsidePopup = props => {
|
|
|
69359
69726
|
calloutIcon,
|
|
69360
69727
|
calloutCloseButton,
|
|
69361
69728
|
calloutCloseByScroll,
|
|
69729
|
+
popupGroup,
|
|
69362
69730
|
...rest
|
|
69363
69731
|
} = props;
|
|
69364
69732
|
const isPopover = mode === "popover";
|
|
69365
69733
|
const isCallout = mode === "callout";
|
|
69366
69734
|
return jsx(Next, {
|
|
69367
69735
|
"aria-haspopup": isPopover ? "listbox" : "dialog",
|
|
69368
|
-
"navi-popover-mode": isPopover ? popoverMode : undefined
|
|
69736
|
+
"navi-popover-mode": isPopover ? popoverMode : undefined
|
|
69737
|
+
// On the trigger, whatever the mode: a peer's popup reads it off the
|
|
69738
|
+
// press that lands here (see `group` in callout.js).
|
|
69739
|
+
,
|
|
69740
|
+
"data-navi-popup-group": popupGroup,
|
|
69369
69741
|
...rest,
|
|
69370
69742
|
// On popupProps already (see the picker's popup assembly); they mean
|
|
69371
69743
|
// nothing to the picker element.
|
|
@@ -69401,7 +69773,12 @@ const PickerContentInsidePopup = props => {
|
|
|
69401
69773
|
status: calloutStatus,
|
|
69402
69774
|
icon: calloutIcon,
|
|
69403
69775
|
closeButton: calloutCloseButton,
|
|
69404
|
-
closeByScroll: calloutCloseByScroll
|
|
69776
|
+
closeByScroll: calloutCloseByScroll
|
|
69777
|
+
// Left unsaid, the callout's own default: no backdrop. It dims
|
|
69778
|
+
// nothing, so the page looks as pressable as it did before.
|
|
69779
|
+
,
|
|
69780
|
+
backdrop: backdrop,
|
|
69781
|
+
group: popupGroup,
|
|
69405
69782
|
children: jsx(PopupModeContext.Provider, {
|
|
69406
69783
|
value: mode,
|
|
69407
69784
|
children: children
|
|
@@ -69438,6 +69815,7 @@ const PickerContentInsidePopup = props => {
|
|
|
69438
69815
|
backdropVariant: backdropVariant,
|
|
69439
69816
|
backdropColor: backdropColor,
|
|
69440
69817
|
backdropFilter: backdropFilter,
|
|
69818
|
+
group: popupGroup,
|
|
69441
69819
|
focusCapture: isPopover ? focusCapture : undefined,
|
|
69442
69820
|
expand: isPopover ? undefined : dialogExpand,
|
|
69443
69821
|
expandX: isPopover ? undefined : dialogExpandX,
|
|
@@ -69497,6 +69875,8 @@ const PickerCalloutPopup = ({
|
|
|
69497
69875
|
icon,
|
|
69498
69876
|
closeButton,
|
|
69499
69877
|
closeByScroll,
|
|
69878
|
+
backdrop,
|
|
69879
|
+
group,
|
|
69500
69880
|
onnavi_request_open,
|
|
69501
69881
|
onnavi_request_close,
|
|
69502
69882
|
onnavi_request_confirm,
|
|
@@ -69538,6 +69918,8 @@ const PickerCalloutPopup = ({
|
|
|
69538
69918
|
icon,
|
|
69539
69919
|
closeButton,
|
|
69540
69920
|
closeByScroll,
|
|
69921
|
+
backdrop,
|
|
69922
|
+
group,
|
|
69541
69923
|
anchorElement,
|
|
69542
69924
|
// The request, chained to the press that made it: the callout reads the
|
|
69543
69925
|
// mousedown off it to wait for the release before listening for a click
|
|
@@ -71704,7 +72086,13 @@ const ListRowContext = createContext(null);
|
|
|
71704
72086
|
// row takes its place in the collection by slot: the place is then the list's
|
|
71705
72087
|
// to move, and the row's to follow — see list_rows.js.
|
|
71706
72088
|
const ListSlotContext = createContext(null);
|
|
71707
|
-
const css$x = /* css */`@
|
|
72089
|
+
const css$x = /* css */`@property --list-group-label-height {
|
|
72090
|
+
syntax: "<length>";
|
|
72091
|
+
inherits: false;
|
|
72092
|
+
initial-value: 0;
|
|
72093
|
+
}
|
|
72094
|
+
|
|
72095
|
+
@layer navi {
|
|
71708
72096
|
.navi_list_container {
|
|
71709
72097
|
--list-outline-width: 1px;
|
|
71710
72098
|
--list-border-radius: 4px;
|
|
@@ -72103,6 +72491,7 @@ const css$x = /* css */`@layer navi {
|
|
|
72103
72491
|
}
|
|
72104
72492
|
|
|
72105
72493
|
& .navi_list_item_group_list {
|
|
72494
|
+
--list-group-label-height: inherit;
|
|
72106
72495
|
flex-direction: column;
|
|
72107
72496
|
width: 100%;
|
|
72108
72497
|
margin: 0;
|
|
@@ -72110,8 +72499,12 @@ const css$x = /* css */`@layer navi {
|
|
|
72110
72499
|
list-style: none;
|
|
72111
72500
|
display: flex;
|
|
72112
72501
|
|
|
72502
|
+
& > .navi_list_item {
|
|
72503
|
+
--list-group-label-height: inherit;
|
|
72504
|
+
}
|
|
72505
|
+
|
|
72113
72506
|
& .navi_list_item {
|
|
72114
|
-
scroll-margin-top: calc(var(--x-list-scroll-spacing-top) + var(--list-group-label-height
|
|
72507
|
+
scroll-margin-top: calc(var(--x-list-scroll-spacing-top) + var(--list-group-label-height));
|
|
72115
72508
|
}
|
|
72116
72509
|
}
|
|
72117
72510
|
|
|
@@ -72263,9 +72656,17 @@ const ListUI = props => {
|
|
|
72263
72656
|
// why not an effect), and the floor of 30 is its: it protects the scrolling
|
|
72264
72657
|
// window, not a picture nobody has seen yet.
|
|
72265
72658
|
const {
|
|
72266
|
-
initial:
|
|
72659
|
+
initial: initialRenderBudgetProp,
|
|
72267
72660
|
after: renderBudgetAfterPaint
|
|
72268
72661
|
} = resolveRenderBudget(renderBudgetProp);
|
|
72662
|
+
// Opening on a position that says how many rows were on screen from its row
|
|
72663
|
+
// on (the way back to where the list was, see reportPosition): that is what
|
|
72664
|
+
// the first picture shows, whatever `initial` guessed — with the row above
|
|
72665
|
+
// when it stood partly in view. The rest waits for the paint like any
|
|
72666
|
+
// `initial`: a page coming back in a route transition builds it inside the
|
|
72667
|
+
// update callback, and every row below the fold delays the movement.
|
|
72668
|
+
const openingPosition = scrolled ?? defaultScrolled;
|
|
72669
|
+
const initialRenderBudget = openingPosition && typeof openingPosition === "object" && typeof openingPosition.visibleCount === "number" ? openingPosition.visibleCount + (openingPosition.offset > 0 ? 1 : 0) : initialRenderBudgetProp;
|
|
72269
72670
|
let renderBudget = renderBudgetAfterPaint;
|
|
72270
72671
|
if (renderBudget < 30 && !renderBudgetSkipCheck) {
|
|
72271
72672
|
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}).`);
|
|
@@ -72728,11 +73129,23 @@ const useListScrollSync = ({
|
|
|
72728
73129
|
horizontal
|
|
72729
73130
|
});
|
|
72730
73131
|
};
|
|
73132
|
+
|
|
73133
|
+
// How many rows the window keeps above the row the list is held at. Around it
|
|
73134
|
+
// for the scrolling to come, from the first paint on; before, the window is
|
|
73135
|
+
// the first picture's, and a picture of the list opening on a row shows that
|
|
73136
|
+
// row and what is below it — with the one above when a remembered position
|
|
73137
|
+
// says it stood partly in view.
|
|
73138
|
+
const rowsAboveOpening = (openAt, windowSize) => {
|
|
73139
|
+
if (renderBudget !== renderBudgetSteady) {
|
|
73140
|
+
return openAt && typeof openAt === "object" && openAt.offset > 0 ? 1 : 0;
|
|
73141
|
+
}
|
|
73142
|
+
return Math.floor(windowSize / 2);
|
|
73143
|
+
};
|
|
72731
73144
|
const [renderWindow, setRenderWindow] = useState(() => {
|
|
72732
73145
|
// Opening somewhere else than the beginning starts by framing there: the
|
|
72733
73146
|
// rows the list will draw are the rows it will ask for.
|
|
72734
73147
|
const openAt = scrolled ?? defaultScrolled;
|
|
72735
|
-
const start = typeof openAt === "number" ? openAt -
|
|
73148
|
+
const start = typeof openAt === "number" ? openAt - rowsAboveOpening(openAt, renderBudget) : 0;
|
|
72736
73149
|
const startClamped = start < 0 ? 0 : start;
|
|
72737
73150
|
return {
|
|
72738
73151
|
start: startClamped,
|
|
@@ -72806,8 +73219,12 @@ const useListScrollSync = ({
|
|
|
72806
73219
|
// Held somewhere it has not reached yet: what the window frames right now
|
|
72807
73220
|
// is not what it will frame, so nothing should be fetched for it.
|
|
72808
73221
|
listRows.holdPending = scrolledWanted !== "start" && scrolledWanted !== undefined;
|
|
73222
|
+
// Zero until the runs have rendered — they count their rows as they do,
|
|
73223
|
+
// after the list — which is not a collection fitting in the window: it is
|
|
73224
|
+
// one whose end is not known yet, and the window then frames the row the
|
|
73225
|
+
// list opens on, already in the first commit.
|
|
72809
73226
|
const total = listRows.totalSignal.peek();
|
|
72810
|
-
if (total <= renderBudget) {
|
|
73227
|
+
if (total > 0 && total <= renderBudget) {
|
|
72811
73228
|
// The whole collection is what the list draws: wherever in it the list is
|
|
72812
73229
|
// held, the window is already its place. Nowhere to move to means nothing
|
|
72813
73230
|
// to wait for — a hold left standing here is a list that never asks for
|
|
@@ -72815,21 +73232,21 @@ const useListScrollSync = ({
|
|
|
72815
73232
|
listRows.holdPending = false;
|
|
72816
73233
|
return;
|
|
72817
73234
|
}
|
|
72818
|
-
const
|
|
73235
|
+
const above = rowsAboveOpening(scrolledWanted, renderBudget);
|
|
72819
73236
|
let wantedStart = null;
|
|
72820
73237
|
if (scrolledWanted === "end") {
|
|
72821
73238
|
wantedStart = total - renderBudget;
|
|
72822
73239
|
} else if (typeof scrolledWanted === "number") {
|
|
72823
|
-
wantedStart = scrolledWanted -
|
|
73240
|
+
wantedStart = scrolledWanted - above;
|
|
72824
73241
|
} else if (scrolledWanted && scrolledWanted.id !== undefined) {
|
|
72825
73242
|
const rowIndex = listRows.locateRow(scrolledWanted.id);
|
|
72826
73243
|
if (rowIndex !== null) {
|
|
72827
|
-
wantedStart = rowIndex -
|
|
73244
|
+
wantedStart = rowIndex - above;
|
|
72828
73245
|
} else if (typeof scrolledWanted.index === "number") {
|
|
72829
73246
|
// The row has not come back yet, but where it stood is known: near
|
|
72830
73247
|
// enough to frame, and to put the scrollbar roughly where it will end
|
|
72831
73248
|
// up rather than at the top.
|
|
72832
|
-
wantedStart = scrolledWanted.index -
|
|
73249
|
+
wantedStart = scrolledWanted.index - above;
|
|
72833
73250
|
}
|
|
72834
73251
|
}
|
|
72835
73252
|
if (wantedStart === null) {
|
|
@@ -72838,12 +73255,12 @@ const useListScrollSync = ({
|
|
|
72838
73255
|
// back — the run asks for it by name (see useRequestMissing).
|
|
72839
73256
|
return;
|
|
72840
73257
|
}
|
|
73258
|
+
if (total > 0 && wantedStart + renderBudget > total) {
|
|
73259
|
+
wantedStart = total - renderBudget;
|
|
73260
|
+
}
|
|
72841
73261
|
if (wantedStart < 0) {
|
|
72842
73262
|
wantedStart = 0;
|
|
72843
73263
|
}
|
|
72844
|
-
if (wantedStart + renderBudget > total) {
|
|
72845
|
-
wantedStart = total - renderBudget;
|
|
72846
|
-
}
|
|
72847
73264
|
const {
|
|
72848
73265
|
start,
|
|
72849
73266
|
end
|
|
@@ -73180,8 +73597,8 @@ const useListScrollSync = ({
|
|
|
73180
73597
|
end
|
|
73181
73598
|
} = renderWindowRef.current;
|
|
73182
73599
|
if (rowIndex < start || rowIndex >= end) {
|
|
73183
|
-
const
|
|
73184
|
-
const wantedStart = rowIndex -
|
|
73600
|
+
const above = rowsAboveOpening(scrolledWanted, end - start);
|
|
73601
|
+
const wantedStart = rowIndex - above < 0 ? 0 : rowIndex - above;
|
|
73185
73602
|
updateRenderWindow(wantedStart, wantedStart + (end - start), `opening on row ${scrolledWanted.id}`);
|
|
73186
73603
|
return;
|
|
73187
73604
|
}
|
|
@@ -73227,6 +73644,15 @@ const useListScrollSync = ({
|
|
|
73227
73644
|
}
|
|
73228
73645
|
};
|
|
73229
73646
|
useLayoutEffect(placeWhereHeld);
|
|
73647
|
+
// Held on a row of a list scrolling the document: where the document goes is
|
|
73648
|
+
// this list's to say, measured on the row, not the url's offset in pixels
|
|
73649
|
+
// (see holdDocumentScroll).
|
|
73650
|
+
useLayoutEffect(() => {
|
|
73651
|
+
if (!heldSomewhere || getScroller() !== document.scrollingElement) {
|
|
73652
|
+
return undefined;
|
|
73653
|
+
}
|
|
73654
|
+
return holdDocumentScroll();
|
|
73655
|
+
}, [heldSomewhere, scrollerElResolved]);
|
|
73230
73656
|
// What to do when the list's own geometry moves under it, kept fresh for the
|
|
73231
73657
|
// observer below (which is installed once).
|
|
73232
73658
|
const onGeometryChangeRef = useRef(null);
|
|
@@ -73269,31 +73695,40 @@ const useListScrollSync = ({
|
|
|
73269
73695
|
// Where the list was at the last thing that moved it. Kept whether anyone
|
|
73270
73696
|
// asked for it or not: it is what a resize needs to put things back.
|
|
73271
73697
|
const positionRef = useRef(null);
|
|
73698
|
+
const captureListAnchor = countVisible => captureScrollAnchor({
|
|
73699
|
+
scrollerEl: getScroller(),
|
|
73700
|
+
listEl: getListEl(),
|
|
73701
|
+
items: listRows.visibleItemsSignal.peek(),
|
|
73702
|
+
horizontal,
|
|
73703
|
+
countVisible
|
|
73704
|
+
});
|
|
73705
|
+
// Said from where the row lands on its own, and with `visibleCount`: the
|
|
73706
|
+
// rows on screen from that one on, which is what a list given this position
|
|
73707
|
+
// back draws first (see ListUI).
|
|
73708
|
+
const toScrolledPosition = position => {
|
|
73709
|
+
const rowEl = findRowElement(getListEl(), position.id);
|
|
73710
|
+
return {
|
|
73711
|
+
id: position.id,
|
|
73712
|
+
index: position.index,
|
|
73713
|
+
offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal),
|
|
73714
|
+
visibleCount: position.visibleCount
|
|
73715
|
+
};
|
|
73716
|
+
};
|
|
73272
73717
|
const reportPosition = () => {
|
|
73273
73718
|
if (!ref.current) {
|
|
73274
73719
|
return;
|
|
73275
73720
|
}
|
|
73276
|
-
const
|
|
73277
|
-
|
|
73278
|
-
|
|
73279
|
-
items: listRows.visibleItemsSignal.peek(),
|
|
73280
|
-
horizontal
|
|
73281
|
-
});
|
|
73721
|
+
const remember = rememberScrollRef.current;
|
|
73722
|
+
const onScrolledChange = onScrolledChangeRef.current;
|
|
73723
|
+
const position = captureListAnchor(remember || Boolean(onScrolledChange));
|
|
73282
73724
|
if (!position) {
|
|
73283
73725
|
return;
|
|
73284
73726
|
}
|
|
73285
73727
|
positionRef.current = position;
|
|
73286
|
-
const remember = rememberScrollRef.current;
|
|
73287
|
-
const onScrolledChange = onScrolledChangeRef.current;
|
|
73288
73728
|
if (!remember && !onScrolledChange) {
|
|
73289
73729
|
return;
|
|
73290
73730
|
}
|
|
73291
|
-
const
|
|
73292
|
-
const scrolledNow = {
|
|
73293
|
-
id: position.id,
|
|
73294
|
-
index: position.index,
|
|
73295
|
-
offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal)
|
|
73296
|
-
};
|
|
73731
|
+
const scrolledNow = toScrolledPosition(position);
|
|
73297
73732
|
if (remember) {
|
|
73298
73733
|
rememberScrollerPosition(listId, scrolledNow);
|
|
73299
73734
|
}
|
|
@@ -73301,6 +73736,20 @@ const useListScrollSync = ({
|
|
|
73301
73736
|
onScrolledChange(scrolledNow);
|
|
73302
73737
|
}
|
|
73303
73738
|
};
|
|
73739
|
+
// Nothing scrolled, yet what is on screen changed: the list has just been
|
|
73740
|
+
// laid out, its rows have arrived, one of them grew. A list read where it
|
|
73741
|
+
// opened would otherwise come back with nothing kept — opening again as a
|
|
73742
|
+
// fresh arrival does, all of its window drawn in the first commit instead of
|
|
73743
|
+
// the rows that were on screen. Kept, and told to nobody: nobody scrolled.
|
|
73744
|
+
const rememberPosition = () => {
|
|
73745
|
+
if (!rememberScrollRef.current || !ref.current) {
|
|
73746
|
+
return;
|
|
73747
|
+
}
|
|
73748
|
+
const position = captureListAnchor(true);
|
|
73749
|
+
if (position) {
|
|
73750
|
+
rememberScrollerPosition(listId, toScrolledPosition(position));
|
|
73751
|
+
}
|
|
73752
|
+
};
|
|
73304
73753
|
// Leaving: with its page, or alone (see forgetScrollerUnlessPageLeft).
|
|
73305
73754
|
useLayoutEffect(() => {
|
|
73306
73755
|
return () => {
|
|
@@ -73326,6 +73775,7 @@ const useListScrollSync = ({
|
|
|
73326
73775
|
// list which has outgrown a bounded ancestor stops holding on to the
|
|
73327
73776
|
// page.
|
|
73328
73777
|
resolveScroller();
|
|
73778
|
+
rememberPosition();
|
|
73329
73779
|
// Two things resize here, and they call for opposite answers. The LIST
|
|
73330
73780
|
// growing is its own content settling: only a list holding itself
|
|
73331
73781
|
// somewhere cares (the end it aims at has moved), and a list the user is
|
|
@@ -74114,12 +74564,15 @@ const getRowName = rowEl => rowEl.getAttribute("navi-list-item-real");
|
|
|
74114
74564
|
// the list is rebuilt around it. Read off the rows' own boxes, not by
|
|
74115
74565
|
// hit-testing the screen: a scrolling list takes its rows out of hit-testing
|
|
74116
74566
|
// (see the navi-scrolling rule in the css above), and the scroll event is
|
|
74117
|
-
// precisely when this is asked.
|
|
74567
|
+
// precisely when this is asked. `countVisible` adds how many rows are on
|
|
74568
|
+
// screen from that one on: what a list coming back there has to draw before
|
|
74569
|
+
// anything else (see the first paint in ListUI).
|
|
74118
74570
|
const captureScrollAnchor = ({
|
|
74119
74571
|
scrollerEl,
|
|
74120
74572
|
listEl,
|
|
74121
74573
|
items,
|
|
74122
|
-
horizontal
|
|
74574
|
+
horizontal,
|
|
74575
|
+
countVisible
|
|
74123
74576
|
}) => {
|
|
74124
74577
|
if (!scrollerEl || !listEl) {
|
|
74125
74578
|
return null;
|
|
@@ -74155,6 +74608,9 @@ const captureScrollAnchor = ({
|
|
|
74155
74608
|
offset
|
|
74156
74609
|
};
|
|
74157
74610
|
if (offset >= 0) {
|
|
74611
|
+
if (countVisible) {
|
|
74612
|
+
anchor.visibleCount = countRowsStartingBefore(rowEls, i, range.to, horizontal);
|
|
74613
|
+
}
|
|
74158
74614
|
return anchor;
|
|
74159
74615
|
}
|
|
74160
74616
|
// The row under the top of the view starts above it. Good enough to hold
|
|
@@ -74163,10 +74619,26 @@ const captureScrollAnchor = ({
|
|
|
74163
74619
|
// top" reads, and can be drawn. Keep looking; this one is the fallback.
|
|
74164
74620
|
if (!fallbackAnchor) {
|
|
74165
74621
|
fallbackAnchor = anchor;
|
|
74622
|
+
if (countVisible) {
|
|
74623
|
+
anchor.visibleCount = countRowsStartingBefore(rowEls, i, range.to, horizontal);
|
|
74624
|
+
}
|
|
74166
74625
|
}
|
|
74167
74626
|
}
|
|
74168
74627
|
return fallbackAnchor;
|
|
74169
74628
|
};
|
|
74629
|
+
// How many of the rows from `fromIndex` on start before `to` along the scroll
|
|
74630
|
+
// axis — the ones on screen, when `to` is where the view ends.
|
|
74631
|
+
const countRowsStartingBefore = (rowEls, fromIndex, to, horizontal) => {
|
|
74632
|
+
let index = fromIndex;
|
|
74633
|
+
while (index < rowEls.length) {
|
|
74634
|
+
const rect = rowEls[index].getBoundingClientRect();
|
|
74635
|
+
if ((horizontal ? rect.left : rect.top) >= to) {
|
|
74636
|
+
break;
|
|
74637
|
+
}
|
|
74638
|
+
index++;
|
|
74639
|
+
}
|
|
74640
|
+
return index - fromIndex;
|
|
74641
|
+
};
|
|
74170
74642
|
// The part of the list that is on screen, along the scrolling axis. Both edges
|
|
74171
74643
|
// matter: the scroller may be larger than the list (scroller="parent") as well
|
|
74172
74644
|
// as smaller (the list scrolls inside its own box).
|
|
@@ -76534,7 +77006,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
76534
77006
|
* are skeletons, the others hold their room. `0` says the list is already
|
|
76535
77007
|
* known to be empty: the empty `fallback` shows right away rather than an
|
|
76536
77008
|
* empty frame.
|
|
76537
|
-
* @param {"start"|"end"|number|{id: string, offset?: number}} [props.defaultScrolled="start"]
|
|
77009
|
+
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.defaultScrolled="start"]
|
|
76538
77010
|
* Where the list opens, after which the user owns the scroll — unless it is
|
|
76539
77011
|
* being come back to (see `scrollResetOnNavigation`). `"end"` is a
|
|
76540
77012
|
* thread read backwards — the last rows are the ones to show, and the ones
|
|
@@ -76546,8 +77018,10 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
76546
77018
|
* the screen it was saved on. `offset: 0` is where a `scrollIntoView()` puts
|
|
76547
77019
|
* it — in front of the fixed bar the scroller gives room for, below the
|
|
76548
77020
|
* sticky header and the group label the row lives under — so nothing of that
|
|
76549
|
-
* room has to be restated as a number by whoever asks.
|
|
76550
|
-
*
|
|
77021
|
+
* room has to be restated as a number by whoever asks. The `index` and
|
|
77022
|
+
* `visibleCount` it also hands out say where to aim before the row is found,
|
|
77023
|
+
* and how many rows to draw before the first paint (see `renderBudget`).
|
|
77024
|
+
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.scrolled]
|
|
76551
77025
|
* The same, but held: the list goes back there every time this changes, even
|
|
76552
77026
|
* after the user has scrolled — the caller owns where the list is (see
|
|
76553
77027
|
* `defaultScrolled` for the uncontrolled form, and `open`/`defaultOpen`
|
|
@@ -76558,12 +77032,12 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
76558
77032
|
* In every form the list holds itself there while it is still finding out
|
|
76559
77033
|
* how many rows there are and how tall one is, and lets go the moment the
|
|
76560
77034
|
* user reaches for the list.
|
|
76561
|
-
* @param {(scrolled: {id: string, index: number, offset: number}) => void} [props.onScrolledChange]
|
|
77035
|
+
* @param {(scrolled: {id: string, index: number, offset: number, visibleCount: number}) => void} [props.onScrolledChange]
|
|
76562
77036
|
* Where the list is, as the user scrolls: the row at the top of the view and
|
|
76563
77037
|
* how far below the place a row lands on its own (see `defaultScrolled`) it
|
|
76564
|
-
* starts. Keep it
|
|
76565
|
-
* later through `scrolled`/`defaultScrolled` — an index
|
|
76566
|
-
* rows get inserted while a list is being read.
|
|
77038
|
+
* starts, and how many rows are on screen from that one on. Keep it whole to
|
|
77039
|
+
* come back to it later through `scrolled`/`defaultScrolled` — an index alone
|
|
77040
|
+
* would not do, since rows get inserted while a list is being read.
|
|
76567
77041
|
* @param {number|{initial: number, after: number}} [props.renderBudget=100]
|
|
76568
77042
|
* How many rows of a `<List.Items>` run are in the DOM at once: the render
|
|
76569
77043
|
* window, which slides as the user scrolls while fillers hold the room of
|
|
@@ -76575,11 +77049,15 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
76575
77049
|
* once, with room for that lookahead — the list warns below 30, and when a
|
|
76576
77050
|
* budget leaves fewer than two rows beyond the screen.
|
|
76577
77051
|
*
|
|
76578
|
-
* `{ initial, after }` for a list
|
|
76579
|
-
*
|
|
76580
|
-
*
|
|
76581
|
-
*
|
|
76582
|
-
*
|
|
77052
|
+
* `{ initial, after }` for a list whose first picture is taken as it is
|
|
77053
|
+
* built — drawn in the click that opens a popup, or in the update callback
|
|
77054
|
+
* of a route transition bringing its page back: `initial` rows in the commit
|
|
77055
|
+
* the browser paints first — about what a phone screen shows — counted from
|
|
77056
|
+
* the row the list opens on, and `after` from the paint on, around it, where
|
|
77057
|
+
* the floor of 30 applies. Opening on a position that says how many rows were
|
|
77058
|
+
* on screen (`visibleCount`, see `onScrolledChange`) draws those first,
|
|
77059
|
+
* whatever `initial` says. The runs ask their source for `after` rows from
|
|
77060
|
+
* the start, so the first picture costs no second request.
|
|
76583
77061
|
* @param {number} [props.virtualItemSize]
|
|
76584
77062
|
* The size of one row along the scroll axis, in px, when every row has the
|
|
76585
77063
|
* same: what the fillers are sized with and what a scroll position is
|
|
@@ -79202,6 +79680,7 @@ const PickerFirstResolver = props => {
|
|
|
79202
79680
|
* escapeEffect?: "cancel" | "close",
|
|
79203
79681
|
* pressOutside?: "close" | "cancel" | "capture",
|
|
79204
79682
|
* backdrop?: boolean,
|
|
79683
|
+
* popupGroup?: string,
|
|
79205
79684
|
* backdropVariant?: "auto" | "lift" | "discrete" | "invisible",
|
|
79206
79685
|
* backdropColor?: string,
|
|
79207
79686
|
* backdropFilter?: string,
|
|
@@ -79601,10 +80080,23 @@ const PickerFirstResolver = props => {
|
|
|
79601
80080
|
* and the page at all. `false` lets a press outside both close the popup and
|
|
79602
80081
|
* reach whatever it landed on, in one gesture — for a picker opened over a
|
|
79603
80082
|
* page that stays as pressable as it looks (a plan, a map, a canvas), where a
|
|
79604
|
-
* wall would spend the first press on dismissing. Honoured in
|
|
80083
|
+
* wall would spend the first press on dismissing. Honoured in every mode: a
|
|
79605
80084
|
* dialog with no wall is shown through the Popover API rather than
|
|
79606
80085
|
* `showModal()`, so a picker docked as a bottom sheet on a phone still
|
|
79607
|
-
* leaves the plan behind it live.
|
|
80086
|
+
* leaves the plan behind it live. `false` by default under `mode="callout"`,
|
|
80087
|
+
* a bubble that dims nothing; `true` there spends the press outside on
|
|
80088
|
+
* closing it — the link under it not followed, the card under it not held —
|
|
80089
|
+
* without a wall: the scroll a finger starts beside the bubble still
|
|
80090
|
+
* scrolls, and a peer (`popupGroup`) still gets its press.
|
|
80091
|
+
* @param {string} [popupGroup] Pickers sharing the name are peers, whatever
|
|
80092
|
+
* their mode: pressing one while another's popup is open goes from that
|
|
80093
|
+
* popup to this one in a single press — a menu bar, where pressing another
|
|
80094
|
+
* menu switches to it. A popover's wall still takes that press — the page
|
|
80095
|
+
* behind stays out of reach, hover included — and hands its click to the
|
|
80096
|
+
* peer it covered; a callout with `backdrop` closes on it without spending
|
|
80097
|
+
* it. Not read in dialog mode: a dialog's wall goes with it on the press
|
|
80098
|
+
* that closes it. Written on the trigger as `data-navi-popup-group`, which
|
|
80099
|
+
* is also how anything else joins the group.
|
|
79608
80100
|
* @param {"auto"|"lift"|"discrete"|"invisible"} [backdropVariant="auto"] How
|
|
79609
80101
|
* visible the popup's backdrop is, independently of what a click outside
|
|
79610
80102
|
* does: `"auto"` is the paint `pressOutside` implies,
|