@jsenv/navi 0.29.415 → 0.29.417
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev/jsenv_navi.js +1524 -1109
- package/dist/dev/jsenv_navi.js.map +18 -16
- package/dist/jsenv_navi.js +955 -554
- package/dist/jsenv_navi.js.map +18 -16
- package/docs/AI_INSTRUCTIONS.md +8 -4
- package/docs/mobile_touch.md +35 -0
- package/docs/resource.md +20 -3
- package/docs/route_transitions.md +29 -7
- package/package.json +2 -2
package/dist/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";
|
|
@@ -15851,235 +15851,857 @@ const useOpenControllerByProps = (props, name) => {
|
|
|
15851
15851
|
};
|
|
15852
15852
|
|
|
15853
15853
|
/**
|
|
15854
|
-
*
|
|
15855
|
-
*
|
|
15854
|
+
* Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
|
|
15855
|
+
* (non-top-layer) renderers — operate on a plain DOM element, no knowledge
|
|
15856
|
+
* of which of the two owns it.
|
|
15857
|
+
*/
|
|
15858
|
+
|
|
15859
|
+
|
|
15860
|
+
/**
|
|
15861
|
+
* Whether a visibleRectEffect delivery is one that can have taken height away
|
|
15862
|
+
* from a popup, and so pushed whatever holds focus out of sight:
|
|
15863
|
+
* - "resize": the window/visual viewport settled — which is also how the
|
|
15864
|
+
* on-screen keyboard arrives, overlay or not (window_size.js);
|
|
15865
|
+
* - ELEMENT_SIZE_CHANGE: the popup's own box measured different;
|
|
15866
|
+
* - "focusin": the focus-settled re-measure, for the room that changes with
|
|
15867
|
+
* nothing announcing it (subscribeFocusSettled in window_size.js).
|
|
15856
15868
|
*
|
|
15857
|
-
*
|
|
15858
|
-
*
|
|
15859
|
-
*
|
|
15860
|
-
|
|
15861
|
-
|
|
15862
|
-
|
|
15863
|
-
|
|
15864
|
-
|
|
15869
|
+
* Everything else is a scroll of one kind or another, where nothing got
|
|
15870
|
+
* smaller and scrolling the focused element back would fight the very gesture
|
|
15871
|
+
* that fired it.
|
|
15872
|
+
*/
|
|
15873
|
+
const mayHaveHiddenFocus = (event) => {
|
|
15874
|
+
const type = event?.type;
|
|
15875
|
+
return (
|
|
15876
|
+
type === "resize" || type === ELEMENT_SIZE_CHANGE || type === "focusin"
|
|
15877
|
+
);
|
|
15878
|
+
};
|
|
15879
|
+
|
|
15880
|
+
const scrollportHeightMap = new WeakMap();
|
|
15881
|
+
/**
|
|
15882
|
+
* Scrolls whatever holds focus inside `popupEl` back into view, if the popup
|
|
15883
|
+
* getting shorter has pushed it out.
|
|
15865
15884
|
*
|
|
15866
|
-
*
|
|
15867
|
-
*
|
|
15868
|
-
*
|
|
15869
|
-
*
|
|
15870
|
-
*
|
|
15871
|
-
*
|
|
15885
|
+
* The case this exists for: a field low in the scrolling body of a popup that
|
|
15886
|
+
* also has a footer (box.jsx — with a body, the body is the only thing that
|
|
15887
|
+
* scrolls and the footer is a sibling sitting right under it). Focusing the
|
|
15888
|
+
* field makes the browser scroll it into view, which it does against the
|
|
15889
|
+
* popup's height AT THAT MOMENT; the on-screen keyboard then opens and takes
|
|
15890
|
+
* that height away. The body shrinks, its scrollTop does not move, so the
|
|
15891
|
+
* content slides down relative to the shorter scrollport and the field ends up
|
|
15892
|
+
* past its bottom edge — visually, swallowed by the footer. The browser does
|
|
15893
|
+
* not redo a scroll-into-view it already answered, so this does.
|
|
15872
15894
|
*
|
|
15873
|
-
*
|
|
15874
|
-
*
|
|
15875
|
-
*
|
|
15876
|
-
*
|
|
15877
|
-
* the
|
|
15878
|
-
*
|
|
15895
|
+
* Only what the shrink itself hid, though: with the keyboard up the user reads
|
|
15896
|
+
* the rest of the popup by scrolling the field away — to reach the submit under
|
|
15897
|
+
* it, typically — and the room keeps changing while they do (a keyboard settling
|
|
15898
|
+
* in two steps, a suggestion strip, a browser bar). Answering each of those by
|
|
15899
|
+
* scrolling the field back takes the popup away from wherever they had just
|
|
15900
|
+
* scrolled it, over and over: what they were reading cannot be reached at all
|
|
15901
|
+
* without blurring the field first, and the popup reads as unscrollable. So the
|
|
15902
|
+
* field is brought back only when it was in view before the room shrank, which
|
|
15903
|
+
* one remembered number answers: a resize moves neither scrollTop nor the
|
|
15904
|
+
* element's offset inside the scrolled content, so measuring against the height
|
|
15905
|
+
* the scrollport HAD is measuring the state before the change.
|
|
15879
15906
|
*
|
|
15880
|
-
*
|
|
15881
|
-
*
|
|
15907
|
+
* Scoped to the field's own scroll container (never the page): a popup traps
|
|
15908
|
+
* scrolling precisely so the document underneath cannot move, and a plain
|
|
15909
|
+
* scrollIntoView walks past a container whose scrollbar isn't visible — see
|
|
15910
|
+
* scrollIntoViewScoped's own doc.
|
|
15911
|
+
*
|
|
15912
|
+
* "nearest": the smallest scroll that makes it visible, and none at all when it
|
|
15913
|
+
* already is. Where it lands is the container's own business — a navi scroller
|
|
15914
|
+
* keeps a band free at its edges so a field never comes back glued to one (see
|
|
15915
|
+
* scroll-padding in box.jsx).
|
|
15882
15916
|
*/
|
|
15883
|
-
const
|
|
15884
|
-
|
|
15885
|
-
|
|
15886
|
-
|
|
15887
|
-
|
|
15888
|
-
|
|
15889
|
-
|
|
15890
|
-
|
|
15891
|
-
|
|
15892
|
-
|
|
15893
|
-
|
|
15894
|
-
|
|
15895
|
-
|
|
15917
|
+
const keepFocusedElementVisible = (popupEl) => {
|
|
15918
|
+
const { activeElement } = document;
|
|
15919
|
+
if (!activeElement || activeElement === popupEl) {
|
|
15920
|
+
return;
|
|
15921
|
+
}
|
|
15922
|
+
if (!popupEl.contains(activeElement)) {
|
|
15923
|
+
return;
|
|
15924
|
+
}
|
|
15925
|
+
const scrollContainer = getScrollContainer(activeElement);
|
|
15926
|
+
if (!scrollContainer || !popupEl.contains(scrollContainer)) {
|
|
15927
|
+
// What scrolls the field is outside the popup, which means the page: a
|
|
15928
|
+
// popup holds it still on purpose (trapScrollInside), so there is nothing
|
|
15929
|
+
// here to scroll back.
|
|
15930
|
+
return;
|
|
15931
|
+
}
|
|
15932
|
+
const scrollportHeight = scrollContainer.clientHeight;
|
|
15933
|
+
const scrollportHeightBefore = scrollportHeightMap.get(scrollContainer);
|
|
15934
|
+
scrollportHeightMap.set(scrollContainer, scrollportHeight);
|
|
15935
|
+
if (scrollportHeightBefore !== undefined) {
|
|
15936
|
+
if (scrollportHeight >= scrollportHeightBefore) {
|
|
15937
|
+
// Nothing was taken away, so nothing was hidden by this.
|
|
15938
|
+
return;
|
|
15939
|
+
}
|
|
15940
|
+
if (
|
|
15941
|
+
!isVisibleInScrollport(
|
|
15942
|
+
activeElement,
|
|
15943
|
+
scrollContainer,
|
|
15944
|
+
scrollportHeightBefore,
|
|
15945
|
+
)
|
|
15946
|
+
) {
|
|
15947
|
+
return;
|
|
15896
15948
|
}
|
|
15897
15949
|
}
|
|
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
|
|
15950
|
+
scrollIntoViewScoped(activeElement, {
|
|
15951
|
+
container: scrollContainer,
|
|
15952
|
+
block: "nearest",
|
|
15917
15953
|
});
|
|
15918
|
-
render(calloutJsx, calloutMessageElement);
|
|
15919
15954
|
};
|
|
15920
15955
|
|
|
15921
|
-
//
|
|
15922
|
-
//
|
|
15923
|
-
//
|
|
15924
|
-
//
|
|
15925
|
-
const
|
|
15926
|
-
|
|
15927
|
-
|
|
15928
|
-
|
|
15929
|
-
const
|
|
15930
|
-
|
|
15931
|
-
|
|
15932
|
-
|
|
15933
|
-
element
|
|
15934
|
-
});
|
|
15956
|
+
// Whether any part of `el` was showing in `container` back when its scrollport
|
|
15957
|
+
// was `scrollportHeight` tall. Both boxes are read now: the container may have
|
|
15958
|
+
// moved as well as shrunk (a centered dialog re-centers itself), and the
|
|
15959
|
+
// difference between the two tops is what that move leaves alone.
|
|
15960
|
+
const isVisibleInScrollport = (el, container, scrollportHeight) => {
|
|
15961
|
+
const elRect = el.getBoundingClientRect();
|
|
15962
|
+
const containerRect = container.getBoundingClientRect();
|
|
15963
|
+
const elTop = elRect.top - containerRect.top + container.scrollTop;
|
|
15964
|
+
const elBottom = elTop + elRect.height;
|
|
15965
|
+
const scrollportTop = container.scrollTop;
|
|
15966
|
+
const scrollportBottom = scrollportTop + scrollportHeight;
|
|
15967
|
+
return elBottom > scrollportTop && elTop < scrollportBottom;
|
|
15935
15968
|
};
|
|
15936
15969
|
|
|
15937
|
-
|
|
15938
|
-
|
|
15939
|
-
|
|
15940
|
-
|
|
15941
|
-
|
|
15942
|
-
|
|
15943
|
-
|
|
15944
|
-
|
|
15945
|
-
|
|
15946
|
-
|
|
15947
|
-
|
|
15948
|
-
|
|
15949
|
-
|
|
15970
|
+
/**
|
|
15971
|
+
* Calls `onSettled` once `el`'s current CSS transition is over — via
|
|
15972
|
+
* `transitionend`, with a safety `setTimeout` fallback matching the longest
|
|
15973
|
+
* `transition-duration`, in case nothing actually transitions or an event is
|
|
15974
|
+
* missed.
|
|
15975
|
+
*
|
|
15976
|
+
* Returns a "cancel" function, so a caller whose instance has been superseded
|
|
15977
|
+
* (a fresh open/close about to set its own state) can keep this stale one from
|
|
15978
|
+
* firing later. Cancelling only stops `onSettled`: undoing whatever the caller
|
|
15979
|
+
* did up front is that fresh call's business, not this one's.
|
|
15980
|
+
*/
|
|
15981
|
+
const whenTransitionSettles = (el, onSettled) => {
|
|
15982
|
+
let settled = false;
|
|
15983
|
+
const onTransitionEnd = (transitionEvent) => {
|
|
15984
|
+
if (transitionEvent.target === el) {
|
|
15985
|
+
finish();
|
|
15950
15986
|
}
|
|
15951
|
-
|
|
15952
|
-
|
|
15987
|
+
};
|
|
15988
|
+
const stopWatching = () => {
|
|
15989
|
+
settled = true;
|
|
15990
|
+
el.removeEventListener("transitionend", onTransitionEnd);
|
|
15991
|
+
clearTimeout(safetyTimeoutId);
|
|
15992
|
+
};
|
|
15993
|
+
const finish = () => {
|
|
15994
|
+
if (settled) {
|
|
15995
|
+
return;
|
|
15953
15996
|
}
|
|
15954
|
-
|
|
15955
|
-
|
|
15956
|
-
|
|
15957
|
-
|
|
15958
|
-
|
|
15959
|
-
|
|
15997
|
+
stopWatching();
|
|
15998
|
+
onSettled();
|
|
15999
|
+
};
|
|
16000
|
+
el.addEventListener("transitionend", onTransitionEnd);
|
|
16001
|
+
const durationsInSeconds = getComputedStyle(el)
|
|
16002
|
+
.transitionDuration.split(",")
|
|
16003
|
+
.map((value) => parseFloat(value) || 0);
|
|
16004
|
+
const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
|
|
16005
|
+
const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
|
|
16006
|
+
return () => {
|
|
16007
|
+
if (settled) {
|
|
16008
|
+
return;
|
|
15960
16009
|
}
|
|
15961
|
-
|
|
15962
|
-
}
|
|
15963
|
-
return vnodes;
|
|
16010
|
+
stopWatching();
|
|
16011
|
+
};
|
|
15964
16012
|
};
|
|
15965
16013
|
|
|
15966
|
-
installImportMetaCssBuild(import.meta);
|
|
15967
16014
|
/**
|
|
15968
|
-
*
|
|
15969
|
-
*
|
|
15970
|
-
*
|
|
15971
|
-
*
|
|
15972
|
-
*
|
|
16015
|
+
* Drops the document's text selection when it lives inside `el`, leaving a
|
|
16016
|
+
* selection made elsewhere on the page alone.
|
|
16017
|
+
*
|
|
16018
|
+
* A popup being closed takes its content with it, and a selection is a claim
|
|
16019
|
+
* on content the user can still act on: kept, it would outlive the surface it
|
|
16020
|
+
* was made on — painted on the box for the length of its exit transition, with
|
|
16021
|
+
* the handles and the copy toolbar a phone draws from the live selection
|
|
16022
|
+
* hanging over something that is going away — and reappear with the box on
|
|
16023
|
+
* the next opening. Dropping the selection is the only way to remove that
|
|
16024
|
+
* chrome: a `user-select: none` on the closing box hides the highlight in some
|
|
16025
|
+
* browsers only, and the handles are the browser's own, drawn from the
|
|
16026
|
+
* selection object rather than from any style.
|
|
15973
16027
|
*/
|
|
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);
|
|
16028
|
+
const clearTextSelectionInside = (el) => {
|
|
16029
|
+
const selection = window.getSelection();
|
|
16030
|
+
if (!selection || selection.rangeCount === 0) {
|
|
16031
|
+
return;
|
|
15996
16032
|
}
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
|
|
16033
|
+
const range = selection.getRangeAt(0);
|
|
16034
|
+
if (!el.contains(range.commonAncestorContainer)) {
|
|
16035
|
+
return;
|
|
16000
16036
|
}
|
|
16037
|
+
selection.removeAllRanges();
|
|
16038
|
+
};
|
|
16001
16039
|
|
|
16002
|
-
|
|
16003
|
-
|
|
16004
|
-
|
|
16040
|
+
/**
|
|
16041
|
+
* Disables pointer-events on `el` until its current CSS transition settles —
|
|
16042
|
+
* avoids the cursor changing/something becoming clickable while the popup is
|
|
16043
|
+
* still visually moving into or out of place.
|
|
16044
|
+
*
|
|
16045
|
+
* Returns whenTransitionSettles' own "cancel" function: it doesn't restore
|
|
16046
|
+
* pointer-events, since a fresh call for the next open/close is about to set
|
|
16047
|
+
* its own state.
|
|
16048
|
+
*/
|
|
16049
|
+
const suppressPointerEventsDuringTransition = (el) => {
|
|
16050
|
+
el.style.pointerEvents = "none";
|
|
16051
|
+
return whenTransitionSettles(el, () => {
|
|
16052
|
+
el.style.pointerEvents = "";
|
|
16053
|
+
});
|
|
16054
|
+
};
|
|
16005
16055
|
|
|
16006
|
-
|
|
16007
|
-
|
|
16056
|
+
/**
|
|
16057
|
+
* Hides the backdrop, deferring until the browser's matching "click" fires
|
|
16058
|
+
* when `closeEvent` was triggered by a mousedown (see popover.jsx's top
|
|
16059
|
+
* comment for why) — same capture-phase-on-document pattern as
|
|
16060
|
+
* armSuppressNextOpenRequest in open_controller.js, which a plain timeout
|
|
16061
|
+
* can't safely replace: mouseup (and the click that follows it) can land an
|
|
16062
|
+
* arbitrarily long time after mousedown (the user is still holding the
|
|
16063
|
+
* button down), so a short timeout can fire first and hide the backdrop
|
|
16064
|
+
* before its own click ever arrives. A capture-phase listener on document
|
|
16065
|
+
* fires for every click regardless of what any bubble-phase handler does
|
|
16066
|
+
* downstream, so no fallback timer is needed.
|
|
16067
|
+
*
|
|
16068
|
+
* `hide` is the caller's own way to actually hide the backdrop
|
|
16069
|
+
* (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
|
|
16070
|
+
* for a plain div) — this helper only owns the mousedown/click timing.
|
|
16071
|
+
*
|
|
16072
|
+
* Returns a disarm function (or undefined if hidden immediately), so a
|
|
16073
|
+
* fresh open can cancel a pending hide it's about to make redundant.
|
|
16074
|
+
*/
|
|
16075
|
+
const armPointerDownOutsideClose = (closeEvent, hide) => {
|
|
16076
|
+
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
16077
|
+
if (!mousedownEvent) {
|
|
16078
|
+
hide();
|
|
16079
|
+
return undefined;
|
|
16008
16080
|
}
|
|
16081
|
+
// `hide` is handed the click, for a wall with peers to re-aim it (see
|
|
16082
|
+
// handWallClickToPeer).
|
|
16083
|
+
const onClick = (clickEvent) => {
|
|
16084
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
16085
|
+
hide(clickEvent);
|
|
16086
|
+
};
|
|
16087
|
+
document.addEventListener("click", onClick, { capture: true });
|
|
16088
|
+
return () => {
|
|
16089
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
16090
|
+
};
|
|
16091
|
+
};
|
|
16009
16092
|
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16093
|
+
/**
|
|
16094
|
+
* Maps a positionArea y/x pair to a concrete `navi-animation` value (a
|
|
16095
|
+
* `prefix` plus a direction word), or `null` if both axes overlap the anchor
|
|
16096
|
+
* (no direction at all — that's `resolvedAnimationKind === "scaling"`
|
|
16097
|
+
* territory instead, see resolveAutoAnimationKind below).
|
|
16098
|
+
*
|
|
16099
|
+
* `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
|
|
16100
|
+
* when docked) keeps the word as the compass direction the popup comes
|
|
16101
|
+
* from: placed "top" (a point/corner), it slides in from the top.
|
|
16102
|
+
* `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
|
|
16103
|
+
* direction instead, the opposite compass point: placed "top" of the
|
|
16104
|
+
* anchor, it moves/grows up, away from the anchor (which sits below it).
|
|
16105
|
+
*
|
|
16106
|
+
* "inset-*"/"center" contribute no direction on their axis either way.
|
|
16107
|
+
*/
|
|
16108
|
+
const resolveDirectionValue = (y, x, { prefix }) => {
|
|
16109
|
+
const yWord =
|
|
16110
|
+
y === "top"
|
|
16111
|
+
? prefix === "expand"
|
|
16112
|
+
? "up"
|
|
16113
|
+
: "top"
|
|
16114
|
+
: y === "bottom"
|
|
16115
|
+
? prefix === "expand"
|
|
16116
|
+
? "down"
|
|
16117
|
+
: "bottom"
|
|
16118
|
+
: null;
|
|
16119
|
+
const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
|
|
16120
|
+
if (!yWord && !xWord) {
|
|
16121
|
+
return null;
|
|
16013
16122
|
}
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
height: 100%;
|
|
16019
|
-
}
|
|
16020
|
-
`;
|
|
16123
|
+
return yWord && xWord
|
|
16124
|
+
? `${prefix}-${yWord}-${xWord}`
|
|
16125
|
+
: `${prefix}-${yWord || xWord}`;
|
|
16126
|
+
};
|
|
16021
16127
|
|
|
16022
16128
|
/**
|
|
16023
|
-
*
|
|
16024
|
-
*
|
|
16025
|
-
*
|
|
16026
|
-
*
|
|
16027
|
-
*
|
|
16129
|
+
* Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
|
|
16130
|
+
* — picked for any real anchor, or for a point/corner placed dead-center
|
|
16131
|
+
* (both positionArea axes overlapping — there's no sensible direction to
|
|
16132
|
+
* slide from in that case). "sliding" otherwise. `anchor` is `undefined`
|
|
16133
|
+
* for any no-anchor/docked case (Dialog always, Popover's own custom
|
|
16134
|
+
* renderer when there's no real anchor), so this collapses to "scaling"
|
|
16135
|
+
* there only for the dead-center case, "sliding" otherwise. The two
|
|
16136
|
+
* "overlapping" booleans below describe the *positionArea* itself (a bare
|
|
16137
|
+
* word vs. "inset-"/"center"), not anything about the anchor — they'd
|
|
16138
|
+
* mean exactly the same thing even with no anchor at all, since it's the
|
|
16139
|
+
* position strategy, not the anchor, that decides whether there's a
|
|
16140
|
+
* direction to slide from.
|
|
16028
16141
|
*/
|
|
16029
|
-
const
|
|
16030
|
-
const
|
|
16142
|
+
const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
|
|
16143
|
+
const yIsOverlapping =
|
|
16144
|
+
parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
|
|
16145
|
+
const xIsOverlapping =
|
|
16146
|
+
parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
|
|
16147
|
+
return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
|
|
16148
|
+
};
|
|
16031
16149
|
|
|
16032
16150
|
/**
|
|
16033
|
-
*
|
|
16034
|
-
*
|
|
16035
|
-
*
|
|
16036
|
-
*
|
|
16037
|
-
*
|
|
16038
|
-
*
|
|
16039
|
-
*
|
|
16040
|
-
*
|
|
16151
|
+
* An opening that reaches a popup with no element of its own to show.
|
|
16152
|
+
*
|
|
16153
|
+
* The one cause seen so far, and it is silent: something inside the popup
|
|
16154
|
+
* suspended while its content was being built, and the `<Loading>` that caught
|
|
16155
|
+
* it sits ABOVE the popup. A boundary suspending on an update keeps the page as
|
|
16156
|
+
* it was, as a copy, so what is opened here belongs to a copy nobody owns any
|
|
16157
|
+
* more — and once the data arrives the subtree is rebuilt from scratch, closed
|
|
16158
|
+
* and empty. Nothing is thrown, nothing is drawn, and the press is lost.
|
|
16041
16159
|
*/
|
|
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
|
-
});
|
|
16160
|
+
const warnPopupHasNoElementToOpen = (popupKind) => {
|
|
16161
|
+
console.warn(
|
|
16162
|
+
`[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 }).`,
|
|
16163
|
+
);
|
|
16061
16164
|
};
|
|
16062
16165
|
|
|
16063
|
-
|
|
16166
|
+
/**
|
|
16167
|
+
* A popup reads "outside" from its own border box: what a press lands on
|
|
16168
|
+
* decides nothing on its own (a genuine backdrop press and a press on the
|
|
16169
|
+
* popup's padding both report the popup element as their target, there being
|
|
16170
|
+
* no real ::backdrop node to be one), so the rectangle is what tells them
|
|
16171
|
+
* apart. That holds as long as the box and what the popup paints are the same
|
|
16172
|
+
* thing — which stops being true for a popup with no surface of its own
|
|
16173
|
+
* (`backgroundColor="transparent"`, no shadow, no padding): what the eye reads
|
|
16174
|
+
* as backdrop is then inside the box, and a press there stays a press on the
|
|
16175
|
+
* popup.
|
|
16176
|
+
*
|
|
16177
|
+
* `data-navi-popup-outside` is how a caller says which of its own boxes are
|
|
16178
|
+
* not the surface. It is opt-in because navi cannot infer it — a background
|
|
16179
|
+
* can come from anywhere — while the caller who made the popup see-through
|
|
16180
|
+
* knows exactly which box is decoration and which is paper.
|
|
16181
|
+
*
|
|
16182
|
+
* The marker answers for the element it is on, never for its descendants: a
|
|
16183
|
+
* box painted inside a marked one is still surface, so a row can be marked and
|
|
16184
|
+
* its empty halves read as backdrop while presses on the controls it holds do
|
|
16185
|
+
* not dismiss anything. A descendant with `pointer-events: none` never becomes
|
|
16186
|
+
* a press target at all, so the marked box answers in its place — while the
|
|
16187
|
+
* same declaration on the marked box itself takes it out of hit-testing and
|
|
16188
|
+
* makes the marker unreachable (see warnAboutUnreachableOutsideRegions).
|
|
16189
|
+
*/
|
|
16190
|
+
const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
|
|
16064
16191
|
|
|
16065
|
-
|
|
16066
|
-
|
|
16192
|
+
/**
|
|
16193
|
+
* `data-navi-popup-inside` is the other direction: a box of the PAGE the
|
|
16194
|
+
* caller says belongs to a popup, so a press on it is not outside that popup
|
|
16195
|
+
* even though it lies beyond the border box — a card whose press is what
|
|
16196
|
+
* fills the one panel of a board. Its value names the popup by `id`, the way
|
|
16197
|
+
* `commandfor` does (several ids, space separated), because the exemption is
|
|
16198
|
+
* one popup's: a menu open beside the board still closes on the same press.
|
|
16199
|
+
*
|
|
16200
|
+
* Unlike the outside marker it answers for its whole subtree: the outside
|
|
16201
|
+
* marker carves free space out of a surface whose controls stay surface, this
|
|
16202
|
+
* one adds a whole thing — the card, its count, its buttons — to the popup's
|
|
16203
|
+
* own ground, and nothing in it is a dismissal.
|
|
16204
|
+
*/
|
|
16205
|
+
const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
|
|
16067
16206
|
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
16071
|
-
|
|
16207
|
+
/**
|
|
16208
|
+
* `data-navi-popup-group` names the peers of a popup: the triggers of the other
|
|
16209
|
+
* popups a press goes to from this one — a menu bar, where pressing another
|
|
16210
|
+
* menu while one is open switches to it, in that one press. A trigger joins by
|
|
16211
|
+
* carrying the name (a Picker's `popupGroup` writes it on its own), and a popup
|
|
16212
|
+
* reads it off what the press landed on.
|
|
16213
|
+
*
|
|
16214
|
+
* Returns the peer `target` is in, or null.
|
|
16215
|
+
*/
|
|
16216
|
+
const findPopupPeer = (target, group) => {
|
|
16217
|
+
if (!group || !target || typeof target.closest !== "function") {
|
|
16218
|
+
return null;
|
|
16219
|
+
}
|
|
16220
|
+
return target.closest(`[data-navi-popup-group="${CSS.escape(group)}"]`);
|
|
16221
|
+
};
|
|
16072
16222
|
|
|
16073
|
-
|
|
16074
|
-
|
|
16075
|
-
|
|
16076
|
-
|
|
16077
|
-
|
|
16078
|
-
|
|
16079
|
-
|
|
16080
|
-
|
|
16081
|
-
|
|
16082
|
-
|
|
16223
|
+
/**
|
|
16224
|
+
* The click a wall took, handed to the peer (see findPopupPeer) it covered.
|
|
16225
|
+
*
|
|
16226
|
+
* A wall cannot let one press through and not another: it is what the pointer
|
|
16227
|
+
* hits, and that is the whole of how it spends a press. So it goes on taking
|
|
16228
|
+
* every one — the page behind stays as unhoverable and unpressable as it looks
|
|
16229
|
+
* — and once the press is over, its click is re-aimed at what the wall stood
|
|
16230
|
+
* over at that point, when that is a peer. The click alone, for the reason
|
|
16231
|
+
* transition_press.js gives: a pointer stream made up with no pointer behind it
|
|
16232
|
+
* starts gestures (a hold, a drag) that nothing ever ends, and a click is what
|
|
16233
|
+
* opens a popup's trigger.
|
|
16234
|
+
*
|
|
16235
|
+
* Called with the click that takes the wall down after a press closed the
|
|
16236
|
+
* popup (see armPointerDownOutsideClose), so the peer opens as the popup it
|
|
16237
|
+
* replaces goes.
|
|
16238
|
+
*/
|
|
16239
|
+
const handWallClickToPeer = (
|
|
16240
|
+
clickEvent,
|
|
16241
|
+
{ wallEl, popupEl, anchorEl, group },
|
|
16242
|
+
) => {
|
|
16243
|
+
if (!clickEvent.isTrusted || clickEvent.target !== wallEl) {
|
|
16244
|
+
// A click dispatched by hand is already aimed, and one that did not land
|
|
16245
|
+
// on the wall (a press on the popup's own outside region) was no wall's.
|
|
16246
|
+
return;
|
|
16247
|
+
}
|
|
16248
|
+
// The topmost element under the wall at that point: what the press would
|
|
16249
|
+
// have landed on with no wall. Asked past the wall rather than after hiding
|
|
16250
|
+
// it, which a fading wall may not be yet.
|
|
16251
|
+
const covered = document
|
|
16252
|
+
.elementsFromPoint(clickEvent.clientX, clickEvent.clientY)
|
|
16253
|
+
.find((el) => el !== wallEl && !popupEl.contains(el));
|
|
16254
|
+
const peer = findPopupPeer(covered, group);
|
|
16255
|
+
if (!peer) {
|
|
16256
|
+
return;
|
|
16257
|
+
}
|
|
16258
|
+
if (peer.contains(popupEl) || (anchorEl && peer.contains(anchorEl))) {
|
|
16259
|
+
// The popup's own trigger — the one holding it (a picker renders its
|
|
16260
|
+
// popup inside itself) or the one it opened against: the press through
|
|
16261
|
+
// the wall closed the popup, and handing the trigger its click would open
|
|
16262
|
+
// it again.
|
|
16263
|
+
return;
|
|
16264
|
+
}
|
|
16265
|
+
// The page sees one click, on the peer, rather than two.
|
|
16266
|
+
clickEvent.stopPropagation();
|
|
16267
|
+
// A PointerEvent where the browser makes clicks of them, a MouseEvent
|
|
16268
|
+
// elsewhere: the same kind, copied field by field.
|
|
16269
|
+
const ClickEvent = clickEvent.constructor;
|
|
16270
|
+
covered.dispatchEvent(new ClickEvent(clickEvent.type, clickEvent));
|
|
16271
|
+
};
|
|
16272
|
+
|
|
16273
|
+
/**
|
|
16274
|
+
* What a press landing on a region the caller declared as not-its-surface
|
|
16275
|
+
* does: exactly what the same press on the backdrop would do.
|
|
16276
|
+
*
|
|
16277
|
+
* Lives on the popup's own content element rather than in the backdrop's
|
|
16278
|
+
* handler, for both renderers: the backdrop is a sibling behind the popup
|
|
16279
|
+
* (never an ancestor), so a press inside the popup's box never reaches it.
|
|
16280
|
+
*/
|
|
16281
|
+
const handlePressOnOutsideRegion = (
|
|
16282
|
+
mouseDownEvent,
|
|
16283
|
+
{ popupEl, openController, pressOutside },
|
|
16284
|
+
) => {
|
|
16285
|
+
if (mouseDownEvent.button !== 0) {
|
|
16286
|
+
return;
|
|
16287
|
+
}
|
|
16288
|
+
const { target } = mouseDownEvent;
|
|
16289
|
+
if (!target.hasAttribute(OUTSIDE_REGION_ATTRIBUTE)) {
|
|
16290
|
+
return;
|
|
16291
|
+
}
|
|
16292
|
+
if (openedDuringThisPress(openController)) {
|
|
16293
|
+
// The release of the press that opened it is not somebody dismissing it
|
|
16294
|
+
// (see openedDuringThisPress).
|
|
16295
|
+
return;
|
|
16296
|
+
}
|
|
16297
|
+
// A popup opens inside its opener's own subtree, so a press on a region
|
|
16298
|
+
// belonging to a popup nested in this one bubbles through here too — and it
|
|
16299
|
+
// is a press on what is in front, not on this popup's own decoration.
|
|
16300
|
+
if (
|
|
16301
|
+
target.closest(`[navi-control="dialog"], [navi-control="popover"]`) !==
|
|
16302
|
+
popupEl
|
|
16303
|
+
) {
|
|
16304
|
+
return;
|
|
16305
|
+
}
|
|
16306
|
+
if (pressOutside === "capture") {
|
|
16307
|
+
mouseDownEvent.preventDefault();
|
|
16308
|
+
return;
|
|
16309
|
+
}
|
|
16310
|
+
if (pressOutside === "close" || pressOutside === "cancel") {
|
|
16311
|
+
openController.requestClose(mouseDownEvent, {
|
|
16312
|
+
isCancel: pressOutside === "cancel",
|
|
16313
|
+
});
|
|
16314
|
+
}
|
|
16315
|
+
};
|
|
16316
|
+
|
|
16317
|
+
/**
|
|
16318
|
+
* `pointer-events: none` and `inert` are the two things reached for to say
|
|
16319
|
+
* "this part of my box is not really there", and neither reaches the popup:
|
|
16320
|
+
* inert speaks to the keyboard and to assistive technology, and
|
|
16321
|
+
* pointer-events takes the box out of hit-testing so the press is answered by
|
|
16322
|
+
* an ancestor that is still the popup's own surface. Writing them next to the
|
|
16323
|
+
* marker is the natural mistake, and it makes the marker silently unreachable
|
|
16324
|
+
* — so it is said out loud, once per open.
|
|
16325
|
+
*/
|
|
16326
|
+
const warnAboutUnreachableOutsideRegions = (popupEl) => {
|
|
16327
|
+
for (const regionEl of popupEl.querySelectorAll(
|
|
16328
|
+
`[${OUTSIDE_REGION_ATTRIBUTE}]`,
|
|
16329
|
+
)) {
|
|
16330
|
+
if (getComputedStyle(regionEl).pointerEvents === "none") {
|
|
16331
|
+
console.warn(
|
|
16332
|
+
`[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.`,
|
|
16333
|
+
regionEl,
|
|
16334
|
+
);
|
|
16335
|
+
}
|
|
16336
|
+
}
|
|
16337
|
+
};
|
|
16338
|
+
|
|
16339
|
+
/**
|
|
16340
|
+
* Hears an outside press from the document itself, for a popup with no
|
|
16341
|
+
* backdrop of its own to catch one: a modal `<dialog>` (whose native
|
|
16342
|
+
* `::backdrop` dispatches nothing a handler can be put on) and any popup the
|
|
16343
|
+
* caller asked to leave the page reachable (`backdrop={false}`).
|
|
16344
|
+
*
|
|
16345
|
+
* The press is read and never taken: no `preventDefault`, no
|
|
16346
|
+
* `stopPropagation`, and the listener is on `document` rather than on
|
|
16347
|
+
* anything covering the page — so what the press landed on hears it too, in
|
|
16348
|
+
* the same gesture, and the popup only takes note. That is the whole point of
|
|
16349
|
+
* a popup with no wall: a press outside is a press on the page, and the page
|
|
16350
|
+
* answers it. The one popup where that cannot hold is the modal dialog, where
|
|
16351
|
+
* the browser has already made everything behind inert before this runs.
|
|
16352
|
+
*
|
|
16353
|
+
* `pressEventType` says which event IS the press, and the two callers want
|
|
16354
|
+
* opposite ones:
|
|
16355
|
+
*
|
|
16356
|
+
* - `"pointerdown"` for a popup with no wall: a page with no wall over it is
|
|
16357
|
+
* a page whose own elements arbitrate their presses, and cancelling a
|
|
16358
|
+
* `pointerdown` — what a drag source and a control keeping the focus where
|
|
16359
|
+
* it is both do — suppresses every compatibility mouse event that would have
|
|
16360
|
+
* followed it. The press happens, reaches its target, and no `mousedown` is
|
|
16361
|
+
* ever dispatched for it. Reading the pointer event is reading the press
|
|
16362
|
+
* itself, whatever anything does with it afterwards, and it is the same
|
|
16363
|
+
* event the drag and surface detectors read.
|
|
16364
|
+
* - `"mousedown"` for the modal dialog, whose native `::backdrop` exists to
|
|
16365
|
+
* spend the press. The page's controls act on `mousedown`, and on a touch
|
|
16366
|
+
* screen that is not the same moment as `pointerdown`: the compatibility
|
|
16367
|
+
* mouse events are synthesized at `touchend` and hit-tested again then.
|
|
16368
|
+
* Closing on `pointerdown` takes the `::backdrop` away before that second
|
|
16369
|
+
* hit-test, so the `mousedown` lands on whatever the page holds under the
|
|
16370
|
+
* finger, and one tap both dismisses the dialog and presses the page. Read
|
|
16371
|
+
* on `mousedown`, the wall is still up when the press the page acts on is
|
|
16372
|
+
* aimed, and it catches it. Nothing behind a modal wall can cancel that
|
|
16373
|
+
* press, since nothing behind it hears the `pointerdown` at all.
|
|
16374
|
+
*
|
|
16375
|
+
* Capture phase, so a handler downstream that stops propagation cannot keep
|
|
16376
|
+
* the popup open either — the press is still outside whatever is made of it.
|
|
16377
|
+
*
|
|
16378
|
+
* Returns a cleanup that removes the listener.
|
|
16379
|
+
*/
|
|
16380
|
+
const armOutsidePressClose = (
|
|
16381
|
+
popupEl,
|
|
16382
|
+
{ openController, pressOutside, pressEventType = "pointerdown" },
|
|
16383
|
+
) => {
|
|
16384
|
+
const onDocumentPointerDown = (pointerDownEvent) => {
|
|
16385
|
+
if (pointerDownEvent.button !== 0) {
|
|
16386
|
+
return;
|
|
16387
|
+
}
|
|
16388
|
+
if (openedDuringThisPress(openController)) {
|
|
16389
|
+
// The release of the press that opened it is not somebody dismissing it
|
|
16390
|
+
// (see openedDuringThisPress).
|
|
16391
|
+
return;
|
|
16392
|
+
}
|
|
16393
|
+
// The press landed inside another popup: that is a press on what is in
|
|
16394
|
+
// front, not outside. Asking the target where it lives rather than asking
|
|
16395
|
+
// this popup whether it was pushed — a popup in front does not have to be
|
|
16396
|
+
// one this popup knows about. In front means neither inside nor around: a
|
|
16397
|
+
// popup nested in this one falls to the containment check below as the
|
|
16398
|
+
// inside press it is, and one this popup opened INSIDE (a bubble rendered
|
|
16399
|
+
// in a sheet's content) is around it, so what that popup holds is outside
|
|
16400
|
+
// this one exactly like the rest of the page.
|
|
16401
|
+
const popupUnderPointer = pointerDownEvent.target.closest?.(
|
|
16402
|
+
`[navi-control="dialog"], [navi-control="popover"]`,
|
|
16403
|
+
);
|
|
16404
|
+
if (
|
|
16405
|
+
popupUnderPointer &&
|
|
16406
|
+
popupUnderPointer !== popupEl &&
|
|
16407
|
+
!popupEl.contains(popupUnderPointer) &&
|
|
16408
|
+
!popupUnderPointer.contains(popupEl)
|
|
16409
|
+
) {
|
|
16410
|
+
return;
|
|
16411
|
+
}
|
|
16412
|
+
if (
|
|
16413
|
+
popupEl.id &&
|
|
16414
|
+
pointerDownEvent.target.closest?.(
|
|
16415
|
+
`[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupEl.id)}"]`,
|
|
16416
|
+
)
|
|
16417
|
+
) {
|
|
16418
|
+
return;
|
|
16419
|
+
}
|
|
16420
|
+
// Real DOM containment wins over the rectangle below — an element
|
|
16421
|
+
// genuinely inside the popup (`overflow: visible`, a negative margin, an
|
|
16422
|
+
// absolutely-positioned child) can be painted outside its border box, and
|
|
16423
|
+
// a press there is not outside just because its coordinates are. The
|
|
16424
|
+
// popup element itself is excluded (contains() answers true for it): a
|
|
16425
|
+
// press on a modal dialog's native backdrop reports the dialog as its
|
|
16426
|
+
// target, there being no `::backdrop` node to be one, so treating that as
|
|
16427
|
+
// contained would keep the rectangle from ever running.
|
|
16428
|
+
if (
|
|
16429
|
+
pointerDownEvent.target !== popupEl &&
|
|
16430
|
+
popupEl.contains(pointerDownEvent.target)
|
|
16431
|
+
) {
|
|
16432
|
+
return;
|
|
16433
|
+
}
|
|
16434
|
+
const rect = popupEl.getBoundingClientRect();
|
|
16435
|
+
const isOutside =
|
|
16436
|
+
pointerDownEvent.clientX < rect.left ||
|
|
16437
|
+
pointerDownEvent.clientX > rect.right ||
|
|
16438
|
+
pointerDownEvent.clientY < rect.top ||
|
|
16439
|
+
pointerDownEvent.clientY > rect.bottom;
|
|
16440
|
+
if (!isOutside) {
|
|
16441
|
+
return;
|
|
16442
|
+
}
|
|
16443
|
+
openController.requestClose(pointerDownEvent, {
|
|
16444
|
+
isCancel: pressOutside === "cancel",
|
|
16445
|
+
});
|
|
16446
|
+
};
|
|
16447
|
+
document.addEventListener(pressEventType, onDocumentPointerDown, {
|
|
16448
|
+
capture: true,
|
|
16449
|
+
});
|
|
16450
|
+
return () => {
|
|
16451
|
+
document.removeEventListener(pressEventType, onDocumentPointerDown, {
|
|
16452
|
+
capture: true,
|
|
16453
|
+
});
|
|
16454
|
+
};
|
|
16455
|
+
};
|
|
16456
|
+
|
|
16457
|
+
/**
|
|
16458
|
+
* Typography reset shared by the surfaces a control opens: callout, popover,
|
|
16459
|
+
* dialog.
|
|
16460
|
+
*
|
|
16461
|
+
* A surface is painted on top of the page but lives in the DOM subtree of the
|
|
16462
|
+
* element it opens from, so every inherited text property of that element
|
|
16463
|
+
* reaches it. The ink an element chose for its own background — centered,
|
|
16464
|
+
* shadowed, uppercase, letter-spaced, kept on one line — arrives on paper that
|
|
16465
|
+
* has none of that background, and the caller discovers it as a symptom
|
|
16466
|
+
* (a blurred tooltip, a centered message) with nothing on screen pointing back
|
|
16467
|
+
* to the rule three elements up. A surface writes its own text, on its own
|
|
16468
|
+
* paper, in the document's terms.
|
|
16469
|
+
*
|
|
16470
|
+
* Every one of them is set to its initial value rather than `revert`: only
|
|
16471
|
+
* `color` and `background-color` are declared by the UA on a `[popover]`/
|
|
16472
|
+
* `<dialog>`, so `revert` on any of these would roll back to the inherited
|
|
16473
|
+
* value — the very thing to stop. `text-align: initial` is `start`, which
|
|
16474
|
+
* still reads the surface's own `direction`, so an RTL document keeps its
|
|
16475
|
+
* side.
|
|
16476
|
+
*
|
|
16477
|
+
* `font-family` is deliberately absent: a surface keeps the face of what
|
|
16478
|
+
* opened it, so a section written in a display font gets its tooltips in that
|
|
16479
|
+
* font. `color`, `font-size` and `font-weight` are absent too — each surface
|
|
16480
|
+
* answers those its own way (a popup writes in --navi-popup-color and follows
|
|
16481
|
+
* the size of what opened it; a callout reverts to the document's ink and
|
|
16482
|
+
* size).
|
|
16483
|
+
*
|
|
16484
|
+
* In `@layer navi` so an app that wants one of these back says so on the
|
|
16485
|
+
* surface and wins, without having to out-specify anything.
|
|
16486
|
+
*/
|
|
16487
|
+
const surfaceTextCss = /* css */ `
|
|
16488
|
+
@layer navi {
|
|
16489
|
+
.navi_callout,
|
|
16490
|
+
.navi_popover,
|
|
16491
|
+
.navi_dialog {
|
|
16492
|
+
font-style: initial;
|
|
16493
|
+
text-align: initial;
|
|
16494
|
+
text-indent: initial;
|
|
16495
|
+
text-transform: initial;
|
|
16496
|
+
text-shadow: none;
|
|
16497
|
+
white-space: initial;
|
|
16498
|
+
word-spacing: initial;
|
|
16499
|
+
letter-spacing: initial;
|
|
16500
|
+
}
|
|
16501
|
+
}
|
|
16502
|
+
`;
|
|
16503
|
+
|
|
16504
|
+
const CalloutContext = createContext();
|
|
16505
|
+
const useCalloutRequestClose = () => {
|
|
16506
|
+
return useContext(CalloutContext)?.requestClose;
|
|
16507
|
+
};
|
|
16508
|
+
const useCalloutElement = () => {
|
|
16509
|
+
return useContext(CalloutContext)?.element;
|
|
16510
|
+
};
|
|
16511
|
+
const renderIntoCallout = (jsx$1, calloutMessageElement, {
|
|
16512
|
+
requestClose,
|
|
16513
|
+
element
|
|
16514
|
+
}) => {
|
|
16515
|
+
const calloutJsx = jsx(CalloutContext.Provider, {
|
|
16516
|
+
value: {
|
|
16517
|
+
requestClose,
|
|
16518
|
+
element
|
|
16519
|
+
},
|
|
16520
|
+
children: jsx$1
|
|
16521
|
+
});
|
|
16522
|
+
render(calloutJsx, calloutMessageElement);
|
|
16523
|
+
};
|
|
16524
|
+
|
|
16525
|
+
// An HTML message is rendered through preact rather than innerHTML: what a
|
|
16526
|
+
// message holds is then a preact tree like any other, so a callout's own JSX
|
|
16527
|
+
// (its context, its close button) works the same whether the message arrived
|
|
16528
|
+
// as markup or as elements.
|
|
16529
|
+
const renderHtmlIntoCallout = (html, calloutMessageElement, {
|
|
16530
|
+
requestClose,
|
|
16531
|
+
element
|
|
16532
|
+
}) => {
|
|
16533
|
+
const template = document.createElement("template");
|
|
16534
|
+
template.innerHTML = html;
|
|
16535
|
+
renderIntoCallout(domToVNodes(template.content), calloutMessageElement, {
|
|
16536
|
+
requestClose,
|
|
16537
|
+
element
|
|
16538
|
+
});
|
|
16539
|
+
};
|
|
16540
|
+
|
|
16541
|
+
// Unmounts whatever preact rendered before the element is filled by hand
|
|
16542
|
+
// (a DOM node, an iframe); innerHTML alone would leave preact believing its
|
|
16543
|
+
// tree is still there.
|
|
16544
|
+
const clearCalloutMessage = calloutMessageElement => {
|
|
16545
|
+
render(null, calloutMessageElement);
|
|
16546
|
+
calloutMessageElement.innerHTML = "";
|
|
16547
|
+
};
|
|
16548
|
+
const domToVNodes = node => {
|
|
16549
|
+
const vnodes = [];
|
|
16550
|
+
for (const child of node.childNodes) {
|
|
16551
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
16552
|
+
vnodes.push(child.data);
|
|
16553
|
+
continue;
|
|
16554
|
+
}
|
|
16555
|
+
if (child.nodeType !== Node.ELEMENT_NODE) {
|
|
16556
|
+
continue;
|
|
16557
|
+
}
|
|
16558
|
+
const props = {};
|
|
16559
|
+
for (const {
|
|
16560
|
+
name,
|
|
16561
|
+
value
|
|
16562
|
+
} of child.attributes) {
|
|
16563
|
+
props[name] = value;
|
|
16564
|
+
}
|
|
16565
|
+
vnodes.push(h(child.localName, props, ...domToVNodes(child)));
|
|
16566
|
+
}
|
|
16567
|
+
return vnodes;
|
|
16568
|
+
};
|
|
16569
|
+
|
|
16570
|
+
installImportMetaCssBuild(import.meta);
|
|
16571
|
+
/**
|
|
16572
|
+
* The icon a callout shows beside its message — a square in the status color
|
|
16573
|
+
* with the "!" glyph — drawable on its own, where the callout is not: on the
|
|
16574
|
+
* trigger that opens one (a picker in callout mode), so what one presses looks
|
|
16575
|
+
* like what it opens. The callout's own template draws from the same glyph
|
|
16576
|
+
* (see calloutTemplate in callout.js).
|
|
16577
|
+
*/
|
|
16578
|
+
|
|
16579
|
+
const css$15 = /* css */`.navi_callout_status_icon {
|
|
16580
|
+
--x-callout-status-icon-color: var(--navi-callout-neutral-color);
|
|
16581
|
+
box-sizing: border-box;
|
|
16582
|
+
aspect-ratio: 1;
|
|
16583
|
+
color: #fff;
|
|
16584
|
+
vertical-align: middle;
|
|
16585
|
+
background-color: var(--x-callout-status-icon-color);
|
|
16586
|
+
border-radius: 2px;
|
|
16587
|
+
flex-shrink: 0;
|
|
16588
|
+
justify-content: center;
|
|
16589
|
+
align-items: center;
|
|
16590
|
+
width: 1em;
|
|
16591
|
+
height: 1em;
|
|
16592
|
+
display: inline-flex;
|
|
16593
|
+
|
|
16594
|
+
&[data-status="success"] {
|
|
16595
|
+
--x-callout-status-icon-color: var(--navi-callout-success-color);
|
|
16596
|
+
}
|
|
16597
|
+
|
|
16598
|
+
&[data-status="info"] {
|
|
16599
|
+
--x-callout-status-icon-color: var(--navi-callout-info-color);
|
|
16600
|
+
}
|
|
16601
|
+
|
|
16602
|
+
&[data-status="warning"] {
|
|
16603
|
+
--x-callout-status-icon-color: var(--navi-callout-warning-color);
|
|
16604
|
+
}
|
|
16605
|
+
|
|
16606
|
+
&[data-status="error"] {
|
|
16607
|
+
--x-callout-status-icon-color: var(--navi-callout-error-color);
|
|
16608
|
+
}
|
|
16609
|
+
|
|
16610
|
+
&[data-shape="circle"] {
|
|
16611
|
+
border-radius: 50%;
|
|
16612
|
+
}
|
|
16613
|
+
|
|
16614
|
+
& svg {
|
|
16615
|
+
width: 100%;
|
|
16616
|
+
height: 100%;
|
|
16617
|
+
}
|
|
16618
|
+
}
|
|
16619
|
+
|
|
16620
|
+
.navi_icon > .navi_callout_status_icon {
|
|
16621
|
+
width: 100%;
|
|
16622
|
+
height: 100%;
|
|
16623
|
+
}
|
|
16624
|
+
`;
|
|
16625
|
+
|
|
16626
|
+
/**
|
|
16627
|
+
* The "!" on a square, viewBox and path both. The box is square and holds the
|
|
16628
|
+
* glyph's margin: the ink is 298 of 540 units tall (55%), centered. Drawn this
|
|
16629
|
+
* way rather than tight around the ink so whoever shows it can give the svg the
|
|
16630
|
+
* whole square — the glyph is then placed by the vector rasteriser, which has
|
|
16631
|
+
* no device pixel grid to miss, at any square size down to a fractional one.
|
|
16632
|
+
*/
|
|
16633
|
+
const CALLOUT_STATUS_GLYPH_VIEWBOX = "0 0 540 540";
|
|
16634
|
+
const CALLOUT_STATUS_GLYPH_PATH = "m232.5,121 8,196h59l8-196zm36.5,224a37,37 0 1,0 2,0z";
|
|
16635
|
+
|
|
16636
|
+
/**
|
|
16637
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
16638
|
+
* status?: "info" | "warning" | "error" | "success" | "none",
|
|
16639
|
+
* shape?: "square" | "circle",
|
|
16640
|
+
* }>}
|
|
16641
|
+
* @param {"info"|"warning"|"error"|"success"|"none"} [status="info"] The color
|
|
16642
|
+
* — the callout's own for that status. `"none"` is the neutral one.
|
|
16643
|
+
* @param {"square"|"circle"} [shape="square"] Square like the callout's own
|
|
16644
|
+
* icon, or a circle.
|
|
16645
|
+
*/
|
|
16646
|
+
const CalloutStatusIcon = ({
|
|
16647
|
+
status = "info",
|
|
16648
|
+
shape = "square"
|
|
16649
|
+
}) => {
|
|
16650
|
+
import.meta.css = [css$15, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
|
|
16651
|
+
return jsx("span", {
|
|
16652
|
+
className: "navi_callout_status_icon",
|
|
16653
|
+
"data-status": status === "none" ? undefined : status,
|
|
16654
|
+
"data-shape": shape === "circle" ? "circle" : undefined,
|
|
16655
|
+
"aria-hidden": "true",
|
|
16656
|
+
children: jsx("svg", {
|
|
16657
|
+
viewBox: CALLOUT_STATUS_GLYPH_VIEWBOX,
|
|
16658
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
16659
|
+
children: jsx("path", {
|
|
16660
|
+
fill: "currentColor",
|
|
16661
|
+
d: CALLOUT_STATUS_GLYPH_PATH
|
|
16662
|
+
})
|
|
16663
|
+
})
|
|
16664
|
+
});
|
|
16665
|
+
};
|
|
16666
|
+
|
|
16667
|
+
installImportMetaCssBuild(import.meta);
|
|
16668
|
+
|
|
16669
|
+
// Unique for the page's lifetime: a caller may write the id in a commandfor.
|
|
16670
|
+
let calloutCount = 0;
|
|
16671
|
+
|
|
16672
|
+
// How far `closeByScroll: true` lets the page move under the callout before
|
|
16673
|
+
// dismissing it: small enough that any deliberate scroll gesture reaches it,
|
|
16674
|
+
// large enough to survive the pixel or two a trackpad rests on.
|
|
16675
|
+
const CLOSE_BY_SCROLL_DISTANCE = 10;
|
|
16676
|
+
|
|
16677
|
+
// The events of a press a `backdrop` callout spends, up to its click (which
|
|
16678
|
+
// closes the callout and is spent there), and `pointercancel`, which says the
|
|
16679
|
+
// browser took the press. No touch events: they are the scroll's, and what
|
|
16680
|
+
// listens to them — a list learning the user took its scroll over — is
|
|
16681
|
+
// watching the finger, not answering the press.
|
|
16682
|
+
const SPENT_PRESS_EVENT_TYPES = [
|
|
16683
|
+
"pointerdown",
|
|
16684
|
+
"pointerup",
|
|
16685
|
+
"pointercancel",
|
|
16686
|
+
"mousedown",
|
|
16687
|
+
"mouseup",
|
|
16688
|
+
"contextmenu",
|
|
16689
|
+
];
|
|
16690
|
+
// Where a press lands on something drawn over the page rather than on the page
|
|
16691
|
+
// itself: a popup of navi's (either layer) or a native top-layer element.
|
|
16692
|
+
const LAYER_SELECTOR =
|
|
16693
|
+
'[popover], dialog, [navi-control="popover"], [navi-control="dialog"]';
|
|
16694
|
+
|
|
16695
|
+
const css$14 = /* css */ `
|
|
16696
|
+
/* jsenv-css-opaque: same as dialog.jsx, cascade order. */
|
|
16697
|
+
@layer navi {
|
|
16698
|
+
.navi_callout {
|
|
16699
|
+
/* A callout is parented to what it explains, so it inherits from it — and
|
|
16700
|
+
an element that suppressed text selection (a list row, a drag source)
|
|
16701
|
+
would make its own explanation unselectable. The message is text one
|
|
16702
|
+
copies. Layered: an app that has a reason to lock its own callouts down
|
|
16703
|
+
says so and wins. */
|
|
16704
|
+
user-select: text;
|
|
16083
16705
|
|
|
16084
16706
|
--callout-success-color: var(--navi-callout-success-color);
|
|
16085
16707
|
--callout-info-color: var(--navi-callout-info-color);
|
|
@@ -16358,6 +16980,17 @@ const css$14 = /* css */ `
|
|
|
16358
16980
|
* the callout opened, so scrolling back cancels nothing but a round trip shorter than the
|
|
16359
16981
|
* distance leaves it open. Unlike an outside click it also dismisses an "error" callout: the
|
|
16360
16982
|
* caller asked for it explicitly.
|
|
16983
|
+
* @param {boolean} [options.backdrop=false] - Spend an outside press on closing the callout,
|
|
16984
|
+
* the way a popup's wall spends it: no link followed, no control pressed, no focus moved, no
|
|
16985
|
+
* hold or drag started under it. Without it that press closes the callout and is answered by
|
|
16986
|
+
* what it landed on too. Not a wall, though: the scroll a finger starts beside the callout
|
|
16987
|
+
* still scrolls, a press on a layer drawn over the page (a sheet the callout's content
|
|
16988
|
+
* opened) is that layer's, and a press that does not close the callout (an "error" one) is
|
|
16989
|
+
* not spent either
|
|
16990
|
+
* @param {string} [options.group] - With `backdrop`: the name the callout's peers carry in
|
|
16991
|
+
* `data-navi-popup-group` — the triggers of other popups, callouts or not. A press inside a
|
|
16992
|
+
* peer still closes the callout but is not spent, so the peer it landed on opens its own
|
|
16993
|
+
* popup in the same press — a menu bar, where pressing another menu switches to it
|
|
16361
16994
|
* @param {Event} [options.openingEvent] - The gesture the callout belongs to. It names the
|
|
16362
16995
|
* opener — `currentTarget` while the event is still dispatching, `target` once it is over,
|
|
16363
16996
|
* so awaiting before opening changes nothing — and the opener is the one part of the anchor
|
|
@@ -16422,6 +17055,8 @@ const openCallout = (
|
|
|
16422
17055
|
closeByPressOutside = status === "info",
|
|
16423
17056
|
closeOnFocusLeave = closeByPressOutside,
|
|
16424
17057
|
closeByScroll = false,
|
|
17058
|
+
backdrop = false,
|
|
17059
|
+
group,
|
|
16425
17060
|
closeButton = true,
|
|
16426
17061
|
openingEvent,
|
|
16427
17062
|
reopen = "toggle",
|
|
@@ -16507,6 +17142,9 @@ const openCallout = (
|
|
|
16507
17142
|
reason,
|
|
16508
17143
|
});
|
|
16509
17144
|
};
|
|
17145
|
+
// Asked again at each press rather than once: `update` changes both.
|
|
17146
|
+
const pressOutsideCloses = () =>
|
|
17147
|
+
closeByPressOutside && callout.status !== "error";
|
|
16510
17148
|
const onRequestClose = (event) => {
|
|
16511
17149
|
if (!callout.opened) {
|
|
16512
17150
|
return;
|
|
@@ -16515,10 +17153,7 @@ const openCallout = (
|
|
|
16515
17153
|
const clickOrSpaceOutside =
|
|
16516
17154
|
reason === "click_outside" || reason === "space_outside";
|
|
16517
17155
|
if (clickOrSpaceOutside) {
|
|
16518
|
-
if (!
|
|
16519
|
-
return;
|
|
16520
|
-
}
|
|
16521
|
-
if (callout.status === "error") {
|
|
17156
|
+
if (!pressOutsideCloses()) {
|
|
16522
17157
|
return;
|
|
16523
17158
|
}
|
|
16524
17159
|
} else if (reason === "focus_leave") {
|
|
@@ -16793,16 +17428,36 @@ const openCallout = (
|
|
|
16793
17428
|
}
|
|
16794
17429
|
return openerElement === target || openerElement.contains(target);
|
|
16795
17430
|
};
|
|
17431
|
+
// Whether an outside press on `target` is spent on closing the callout
|
|
17432
|
+
// (`backdrop`). What is not the page behind the callout keeps its press: a
|
|
17433
|
+
// layer the callout is not part of — its own page may well be a sheet, a
|
|
17434
|
+
// sheet its content opened is in front of it — and a peer, which the press
|
|
17435
|
+
// is meant to open.
|
|
17436
|
+
const spendsPressOn = (target) => {
|
|
17437
|
+
if (!backdrop || !pressOutsideCloses()) {
|
|
17438
|
+
return false;
|
|
17439
|
+
}
|
|
17440
|
+
const layer = target.closest?.(LAYER_SELECTOR);
|
|
17441
|
+
if (layer && !layer.contains(calloutElement)) {
|
|
17442
|
+
return false;
|
|
17443
|
+
}
|
|
17444
|
+
if (findPopupPeer(target, group)) {
|
|
17445
|
+
return false;
|
|
17446
|
+
}
|
|
17447
|
+
return true;
|
|
17448
|
+
};
|
|
17449
|
+
const spend = (event) => {
|
|
17450
|
+
debug(event, `outside press spent on closing the callout (backdrop)`);
|
|
17451
|
+
event.preventDefault();
|
|
17452
|
+
event.stopPropagation();
|
|
17453
|
+
};
|
|
16796
17454
|
const handleClickOutside = (event) => {
|
|
16797
17455
|
if (event.button !== 0) {
|
|
16798
17456
|
// right click
|
|
16799
17457
|
return;
|
|
16800
17458
|
}
|
|
16801
17459
|
const clickTarget = event.target;
|
|
16802
|
-
if (
|
|
16803
|
-
clickTarget === calloutElement ||
|
|
16804
|
-
calloutElement.contains(clickTarget)
|
|
16805
|
-
) {
|
|
17460
|
+
if (calloutElement.contains(clickTarget)) {
|
|
16806
17461
|
return;
|
|
16807
17462
|
}
|
|
16808
17463
|
if (isInsideOpener(clickTarget)) {
|
|
@@ -16815,14 +17470,20 @@ const openCallout = (
|
|
|
16815
17470
|
debug(event, `click on opener, let its handler decide`);
|
|
16816
17471
|
return;
|
|
16817
17472
|
}
|
|
17473
|
+
// Read before the close, which takes the callout out of the document —
|
|
17474
|
+
// and out of the layer it stands in, for spendsPressOn to find.
|
|
17475
|
+
const spent = spendsPressOn(clickTarget);
|
|
16818
17476
|
requestClose(event, "click_outside");
|
|
17477
|
+
if (spent) {
|
|
17478
|
+
spend(event);
|
|
17479
|
+
}
|
|
16819
17480
|
};
|
|
16820
17481
|
const handleSpaceOutside = (event) => {
|
|
16821
17482
|
if (event.key !== " ") {
|
|
16822
17483
|
return;
|
|
16823
17484
|
}
|
|
16824
17485
|
const keyTarget = event.target;
|
|
16825
|
-
if (
|
|
17486
|
+
if (calloutElement.contains(keyTarget)) {
|
|
16826
17487
|
return;
|
|
16827
17488
|
}
|
|
16828
17489
|
if (isInsideOpener(keyTarget)) {
|
|
@@ -16831,7 +17492,41 @@ const openCallout = (
|
|
|
16831
17492
|
debug(event, `space on opener, let its handler decide`);
|
|
16832
17493
|
return;
|
|
16833
17494
|
}
|
|
17495
|
+
const spent = spendsPressOn(keyTarget);
|
|
16834
17496
|
requestClose(event, "space_outside");
|
|
17497
|
+
if (spent) {
|
|
17498
|
+
spend(event);
|
|
17499
|
+
}
|
|
17500
|
+
};
|
|
17501
|
+
// The press before its click: a click ends a gesture whose pointerdown
|
|
17502
|
+
// already reached the page, and what acts on the press itself — a hold, a
|
|
17503
|
+
// drag, the focus moving — would otherwise happen under a callout that only
|
|
17504
|
+
// closes on the click. Decided at the pointerdown and kept for the whole
|
|
17505
|
+
// press, wherever its later events land: a release beside the callout ends
|
|
17506
|
+
// a drag started inside it, and must reach it.
|
|
17507
|
+
let pressSpent = false;
|
|
17508
|
+
const handlePressOutside = (event) => {
|
|
17509
|
+
if (event.type === "pointerdown") {
|
|
17510
|
+
const pressTarget = event.target;
|
|
17511
|
+
pressSpent =
|
|
17512
|
+
event.button === 0 &&
|
|
17513
|
+
!calloutElement.contains(pressTarget) &&
|
|
17514
|
+
!isInsideOpener(pressTarget) &&
|
|
17515
|
+
spendsPressOn(pressTarget);
|
|
17516
|
+
} else if (event.type === "pointercancel") {
|
|
17517
|
+
// The browser took the press over — a scroll — and nothing follows it.
|
|
17518
|
+
pressSpent = false;
|
|
17519
|
+
return;
|
|
17520
|
+
}
|
|
17521
|
+
if (!pressSpent) {
|
|
17522
|
+
return;
|
|
17523
|
+
}
|
|
17524
|
+
if (event.type === "mousedown" || event.type === "contextmenu") {
|
|
17525
|
+
// What the press does on its own: the focus moving, a selection
|
|
17526
|
+
// starting, the menu of a held finger.
|
|
17527
|
+
event.preventDefault();
|
|
17528
|
+
}
|
|
17529
|
+
event.stopPropagation();
|
|
16835
17530
|
};
|
|
16836
17531
|
|
|
16837
17532
|
const registerClickOutsideListener = () => {
|
|
@@ -16841,6 +17536,16 @@ const openCallout = (
|
|
|
16841
17536
|
document.removeEventListener("click", handleClickOutside, true);
|
|
16842
17537
|
document.removeEventListener("keydown", handleSpaceOutside, true);
|
|
16843
17538
|
});
|
|
17539
|
+
if (backdrop) {
|
|
17540
|
+
for (const type of SPENT_PRESS_EVENT_TYPES) {
|
|
17541
|
+
document.addEventListener(type, handlePressOutside, true);
|
|
17542
|
+
}
|
|
17543
|
+
addTeardown(() => {
|
|
17544
|
+
for (const type of SPENT_PRESS_EVENT_TYPES) {
|
|
17545
|
+
document.removeEventListener(type, handlePressOutside, true);
|
|
17546
|
+
}
|
|
17547
|
+
});
|
|
17548
|
+
}
|
|
16844
17549
|
};
|
|
16845
17550
|
// A callout opened during a press (mousedown or pointerdown) must wait for the
|
|
16846
17551
|
// matching release before listening for click-outside, otherwise the same
|
|
@@ -18045,7 +18750,7 @@ const warnOnReopenWithinSameEvent = (anchorElement, openingEvent) => {
|
|
|
18045
18750
|
*
|
|
18046
18751
|
* Usage:
|
|
18047
18752
|
* const myToken = createOpenToken();
|
|
18048
|
-
* calloutManager.addOpenToken(myToken, { message, status, testId, closeByScroll, anchorElement, event, skipFocus, onClose });
|
|
18753
|
+
* calloutManager.addOpenToken(myToken, { message, status, testId, closeByScroll, backdrop, group, anchorElement, event, skipFocus, onClose });
|
|
18049
18754
|
* calloutManager.removeOpenToken(myToken, event);
|
|
18050
18755
|
* calloutManager.requestCloseCallout(event, debugReason); // force-close all
|
|
18051
18756
|
* calloutManager.callout // current open callout or null
|
|
@@ -18103,6 +18808,8 @@ const createCalloutManager = (
|
|
|
18103
18808
|
closeButton: tokenData.closeButton,
|
|
18104
18809
|
closeByPressOutside: tokenData.status !== "error",
|
|
18105
18810
|
closeByScroll: tokenData.closeByScroll,
|
|
18811
|
+
backdrop: tokenData.backdrop,
|
|
18812
|
+
group: tokenData.group,
|
|
18106
18813
|
anchorElement,
|
|
18107
18814
|
openingEvent: event,
|
|
18108
18815
|
skipFocus: tokenData.skipFocus,
|
|
@@ -18231,6 +18938,8 @@ const createCalloutManager = (
|
|
|
18231
18938
|
icon,
|
|
18232
18939
|
closeButton,
|
|
18233
18940
|
closeByScroll,
|
|
18941
|
+
backdrop,
|
|
18942
|
+
group,
|
|
18234
18943
|
anchorElement,
|
|
18235
18944
|
event,
|
|
18236
18945
|
skipFocus,
|
|
@@ -18248,6 +18957,8 @@ const createCalloutManager = (
|
|
|
18248
18957
|
icon,
|
|
18249
18958
|
closeButton,
|
|
18250
18959
|
closeByScroll,
|
|
18960
|
+
backdrop,
|
|
18961
|
+
group,
|
|
18251
18962
|
skipFocus,
|
|
18252
18963
|
onClose,
|
|
18253
18964
|
// Resolved as the token is added, and kept with it: a token shown later
|
|
@@ -24480,7 +25191,31 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
24480
25191
|
}
|
|
24481
25192
|
return result;
|
|
24482
25193
|
};
|
|
24483
|
-
|
|
25194
|
+
// A computed of the row with this id, one per id: what a render reads to
|
|
25195
|
+
// follow that row and no other. An upsert replaces only the items it changes
|
|
25196
|
+
// (see assign below), so a row nobody wrote keeps its object, and its
|
|
25197
|
+
// computed stays still while the rest of the store moves — read through
|
|
25198
|
+
// `select`, a render follows the whole store. Kept as long as the store,
|
|
25199
|
+
// like the rows it holds.
|
|
25200
|
+
const itemSignalById = new Map();
|
|
25201
|
+
const itemSignalForId = (id) => {
|
|
25202
|
+
let itemSignal = itemSignalById.get(id);
|
|
25203
|
+
if (!itemSignal) {
|
|
25204
|
+
itemSignal = computed(() => idMapSignal.value.get(id));
|
|
25205
|
+
itemSignalById.set(id, itemSignal);
|
|
25206
|
+
}
|
|
25207
|
+
return itemSignal;
|
|
25208
|
+
};
|
|
25209
|
+
// A value changes a row when it is another object — the app writing a new
|
|
25210
|
+
// one says something changed, and that is taken as said. An answer says it
|
|
25211
|
+
// with every object nested in its rows: JSON.parse makes them all new, the
|
|
25212
|
+
// ones that did not move included, so a revalidation returning the same
|
|
25213
|
+
// rows would replace every one of them for every reader. What an answer
|
|
25214
|
+
// sends again equal in depth is the value already held (see upsertAnswer).
|
|
25215
|
+
const valueChangedByWrite = (newValue, oldValue) => newValue !== oldValue;
|
|
25216
|
+
const valueChangedByAnswer = (newValue, oldValue) =>
|
|
25217
|
+
newValue !== oldValue && !compareTwoJsValues(newValue, oldValue);
|
|
25218
|
+
const upsertWith = (args, valueChanged) => {
|
|
24484
25219
|
const mutationsMap = new Map(); // Map<itemId, propertyMutations>
|
|
24485
25220
|
const triggerPropertyMutations = () => {
|
|
24486
25221
|
for (const itemPropertiesObserver of itemPropertiesObserverSet) {
|
|
@@ -24544,7 +25279,27 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
24544
25279
|
const newValue = props[key];
|
|
24545
25280
|
if (itemOwnKeys.includes(key)) {
|
|
24546
25281
|
const oldValue = item[key];
|
|
24547
|
-
|
|
25282
|
+
const { set } = itemOwnPropertyDescriptors[key];
|
|
25283
|
+
if (set) {
|
|
25284
|
+
// A relation (.one(), .many() and their scoped forms in
|
|
25285
|
+
// resource_graph.js): the row holds which children it points to,
|
|
25286
|
+
// the children live in their own store, and whoever reads them
|
|
25287
|
+
// follows them there. Its setter writes them and says whether the
|
|
25288
|
+
// row points elsewhere now. Compared as a value instead, the
|
|
25289
|
+
// children received are never the object the row hands out, and
|
|
25290
|
+
// every write naming the relation would replace the row.
|
|
25291
|
+
if (set.call(itemWithProps, newValue)) {
|
|
25292
|
+
hasChanges = true;
|
|
25293
|
+
propertyMutations[key] = {
|
|
25294
|
+
oldValue,
|
|
25295
|
+
newValue,
|
|
25296
|
+
target: item,
|
|
25297
|
+
newTarget: itemWithProps,
|
|
25298
|
+
};
|
|
25299
|
+
}
|
|
25300
|
+
continue;
|
|
25301
|
+
}
|
|
25302
|
+
if (valueChanged(newValue, oldValue)) {
|
|
24548
25303
|
hasChanges = true;
|
|
24549
25304
|
itemWithProps[key] = newValue;
|
|
24550
25305
|
propertyMutations[key] = {
|
|
@@ -24696,6 +25451,9 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
24696
25451
|
triggerPropertyMutations();
|
|
24697
25452
|
return item;
|
|
24698
25453
|
};
|
|
25454
|
+
const upsert = (...args) => upsertWith(args, valueChangedByWrite);
|
|
25455
|
+
// The same, for the result of a callback — what the server answered.
|
|
25456
|
+
const upsertAnswer = (...args) => upsertWith(args, valueChangedByAnswer);
|
|
24699
25457
|
const drop = (...args) => {
|
|
24700
25458
|
const removedItemArray = [];
|
|
24701
25459
|
const triggerRemovedMutations = () => {
|
|
@@ -24870,7 +25628,9 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
24870
25628
|
arraySignal,
|
|
24871
25629
|
select,
|
|
24872
25630
|
selectAll,
|
|
25631
|
+
itemSignalForId,
|
|
24873
25632
|
upsert,
|
|
25633
|
+
upsertAnswer,
|
|
24874
25634
|
drop,
|
|
24875
25635
|
|
|
24876
25636
|
observeItemProperties,
|
|
@@ -25628,7 +26388,7 @@ const createRangeReader = (
|
|
|
25628
26388
|
`${actionName} must return { items, start, count }, received ${describeResult(result)}.`,
|
|
25629
26389
|
);
|
|
25630
26390
|
}
|
|
25631
|
-
const items = store.
|
|
26391
|
+
const items = store.upsertAnswer(result.items);
|
|
25632
26392
|
let { start, count } = result;
|
|
25633
26393
|
if (start === undefined) {
|
|
25634
26394
|
const startAsked = rangeParams.start;
|
|
@@ -26035,12 +26795,34 @@ const createResource = (
|
|
|
26035
26795
|
idKey,
|
|
26036
26796
|
uniqueKeys,
|
|
26037
26797
|
|
|
26038
|
-
// Reactive reads, not hooks:
|
|
26039
|
-
//
|
|
26040
|
-
//
|
|
26798
|
+
// Reactive reads, not hooks: nothing hook-shaped inside, so a loop or a
|
|
26799
|
+
// condition is fine. Each subscribes the render that calls it to what it
|
|
26800
|
+
// returns: the rows asked for, each through a computed of that row (see
|
|
26801
|
+
// itemSignalForId in array_signal_store.js), or the whole store for
|
|
26802
|
+
// `useArray` — a card reading its own row stays still while the page next
|
|
26803
|
+
// to it loads rows of its own. `useAllByIds` skips the ids the store does
|
|
26804
|
+
// not hold.
|
|
26041
26805
|
useArray: () => store.arraySignal.value,
|
|
26042
|
-
useById: (id) => store.
|
|
26043
|
-
useAllByIds: (idArray) =>
|
|
26806
|
+
useById: (id) => store.itemSignalForId(id).value,
|
|
26807
|
+
useAllByIds: (idArray) => {
|
|
26808
|
+
const items = [];
|
|
26809
|
+
for (const idOrProps of idArray) {
|
|
26810
|
+
let id = idOrProps;
|
|
26811
|
+
if (isProps(idOrProps) && !Object.hasOwn(idOrProps, idKey)) {
|
|
26812
|
+
// Named by a unique key: which row that is can only be read off
|
|
26813
|
+
// the whole store.
|
|
26814
|
+
const item = store.select(idOrProps);
|
|
26815
|
+
id = item ? item[idKey] : undefined;
|
|
26816
|
+
} else if (isProps(idOrProps)) {
|
|
26817
|
+
id = idOrProps[idKey];
|
|
26818
|
+
}
|
|
26819
|
+
const item = store.itemSignalForId(id).value;
|
|
26820
|
+
if (item) {
|
|
26821
|
+
items.push(item);
|
|
26822
|
+
}
|
|
26823
|
+
}
|
|
26824
|
+
return items;
|
|
26825
|
+
},
|
|
26044
26826
|
|
|
26045
26827
|
withParams: undefined,
|
|
26046
26828
|
one: undefined,
|
|
@@ -26267,7 +27049,7 @@ const createResource = (
|
|
|
26267
27049
|
: // GET/PUT contract (see .one() JSDoc): the parent object with the
|
|
26268
27050
|
// relationship nested inside, or null for no relationship.
|
|
26269
27051
|
(result) => {
|
|
26270
|
-
const item = store.
|
|
27052
|
+
const item = store.upsertAnswer(result);
|
|
26271
27053
|
const childItem = item[propertyName];
|
|
26272
27054
|
return childItem ? childItem[childIdKey] : undefined;
|
|
26273
27055
|
};
|
|
@@ -26448,8 +27230,8 @@ const createResource = (
|
|
|
26448
27230
|
: (childData) => {
|
|
26449
27231
|
// an array is [property, value, props], used to rename the child id
|
|
26450
27232
|
const childItem = Array.isArray(childData)
|
|
26451
|
-
? childStore.
|
|
26452
|
-
: childStore.
|
|
27233
|
+
? childStore.upsertAnswer(...childData)
|
|
27234
|
+
: childStore.upsertAnswer(childData);
|
|
26453
27235
|
return childItem[childIdKey];
|
|
26454
27236
|
};
|
|
26455
27237
|
const throwInvalidResult = createInvalidResultThrower(
|
|
@@ -26491,7 +27273,7 @@ const createResource = (
|
|
|
26491
27273
|
? (result) => {
|
|
26492
27274
|
// GET_MANY contract (see .many() JSDoc): the parent object with
|
|
26493
27275
|
// the child array nested inside; the array replaces the relationship.
|
|
26494
|
-
const item = store.
|
|
27276
|
+
const item = store.upsertAnswer(result);
|
|
26495
27277
|
const childItemArray = item[propertyName];
|
|
26496
27278
|
return childItemArray.map((childItem) => childItem[childIdKey]);
|
|
26497
27279
|
}
|
|
@@ -26522,7 +27304,7 @@ const createResource = (
|
|
|
26522
27304
|
return deletedChildItemIdArray;
|
|
26523
27305
|
}
|
|
26524
27306
|
: (childDataArray) => {
|
|
26525
|
-
const childItemArray = childStore.
|
|
27307
|
+
const childItemArray = childStore.upsertAnswer(childDataArray);
|
|
26526
27308
|
return childItemArray.map((childItem) => childItem[childIdKey]);
|
|
26527
27309
|
};
|
|
26528
27310
|
const throwInvalidResult = createInvalidResultThrower(
|
|
@@ -26661,18 +27443,37 @@ const createResource = (
|
|
|
26661
27443
|
childSetup(childItem);
|
|
26662
27444
|
}
|
|
26663
27445
|
const childSignal = signal(null);
|
|
27446
|
+
// Returns whether the child changed — what the store asks a relation
|
|
27447
|
+
// (see assign in array_signal_store.js). Assigned in place, the child
|
|
27448
|
+
// tells nobody itself: a prop it receives changed is a change of the
|
|
27449
|
+
// owner's row, which is how its readers see it.
|
|
26664
27450
|
const applyProps = (props) => {
|
|
26665
27451
|
if (!props) {
|
|
27452
|
+
if (childSignal.peek() === null) {
|
|
27453
|
+
return false;
|
|
27454
|
+
}
|
|
26666
27455
|
childSignal.value = null;
|
|
26667
|
-
return;
|
|
27456
|
+
return true;
|
|
26668
27457
|
}
|
|
26669
|
-
|
|
27458
|
+
let changed = false;
|
|
27459
|
+
// Assign each prop in place. Reactive setters (from chained .one()
|
|
27460
|
+
// etc.) fire, and say whether their relation changed.
|
|
26670
27461
|
for (const [key, value] of Object.entries(props)) {
|
|
26671
|
-
|
|
27462
|
+
const descriptor = Object.getOwnPropertyDescriptor(childItem, key);
|
|
27463
|
+
if (descriptor && descriptor.set) {
|
|
27464
|
+
if (descriptor.set.call(childItem, value)) {
|
|
27465
|
+
changed = true;
|
|
27466
|
+
}
|
|
27467
|
+
} else if (childItem[key] !== value) {
|
|
27468
|
+
childItem[key] = value;
|
|
27469
|
+
changed = true;
|
|
27470
|
+
}
|
|
26672
27471
|
}
|
|
26673
27472
|
if (childSignal.peek() !== childItem) {
|
|
26674
27473
|
childSignal.value = childItem; // first activation: null → childItem
|
|
27474
|
+
changed = true;
|
|
26675
27475
|
}
|
|
27476
|
+
return changed;
|
|
26676
27477
|
};
|
|
26677
27478
|
if (!ownerName) {
|
|
26678
27479
|
// Declared on a scoped child, an owner id is only unique inside its
|
|
@@ -26960,7 +27761,7 @@ const createResource = (
|
|
|
26960
27761
|
if (isMany) {
|
|
26961
27762
|
// GET_MANY, POST_MANY, PUT_MANY etc: rest[0] is the array of items,
|
|
26962
27763
|
// and it replaces the whole collection.
|
|
26963
|
-
const itemArray = childStore.
|
|
27764
|
+
const itemArray = childStore.upsertAnswer(rest[0]);
|
|
26964
27765
|
const idArray = itemArray.map((childItem) => childItem[childIdKey]);
|
|
26965
27766
|
idArraySignal.value = idArray;
|
|
26966
27767
|
return [ownerId, idArray];
|
|
@@ -26969,8 +27770,8 @@ const createResource = (
|
|
|
26969
27770
|
// GET, POST, PUT, PATCH: rest may be [props] or [oldId, props] for renames
|
|
26970
27771
|
const childItem =
|
|
26971
27772
|
rest.length > 1
|
|
26972
|
-
? childStore.
|
|
26973
|
-
: childStore.
|
|
27773
|
+
? childStore.upsertAnswer(...rest)
|
|
27774
|
+
: childStore.upsertAnswer(rest[0]);
|
|
26974
27775
|
return [ownerId, childItem[childIdKey]];
|
|
26975
27776
|
},
|
|
26976
27777
|
valueToData: (value) => {
|
|
@@ -27106,8 +27907,8 @@ const createRestActionFactoryForRoot = (
|
|
|
27106
27907
|
// An array result is [property, value, props] — used to rename the
|
|
27107
27908
|
// idKey of an item: store.upsert("name", "currentName", { name: "newName" })
|
|
27108
27909
|
const item = Array.isArray(result)
|
|
27109
|
-
? store.
|
|
27110
|
-
: store.
|
|
27910
|
+
? store.upsertAnswer(...result)
|
|
27911
|
+
: store.upsertAnswer(result);
|
|
27111
27912
|
return item[idKey];
|
|
27112
27913
|
};
|
|
27113
27914
|
const throwInvalidResult = createInvalidResultThrower(
|
|
@@ -27171,7 +27972,7 @@ const createRestActionFactoryForRoot = (
|
|
|
27171
27972
|
verb === "DELETE"
|
|
27172
27973
|
? (idOrMutableIdArray) => store.drop(idOrMutableIdArray)
|
|
27173
27974
|
: (dataArray) => {
|
|
27174
|
-
const itemArray = store.
|
|
27975
|
+
const itemArray = store.upsertAnswer(dataArray);
|
|
27175
27976
|
return itemArray.map((item) => item[idKey]);
|
|
27176
27977
|
};
|
|
27177
27978
|
|
|
@@ -27300,14 +28101,17 @@ ${relationLabel} source location: ${declarationSite}`,
|
|
|
27300
28101
|
// array of child props/ids, or anything else meaning "empty") into an array of
|
|
27301
28102
|
// child ids, upserting each entry into the child store. The id array signal is
|
|
27302
28103
|
// only touched when the resulting ids actually differ.
|
|
28104
|
+
// Returns whether the children the row points to changed — what the store asks
|
|
28105
|
+
// a relation (see assign in array_signal_store.js).
|
|
27303
28106
|
const createChildIdArrayUpdater = (childStore, childIdKey, idArraySignal) => {
|
|
27304
28107
|
return (valueArray) => {
|
|
27305
28108
|
const currentIdArray = idArraySignal.peek();
|
|
27306
28109
|
if (!Array.isArray(valueArray)) {
|
|
27307
28110
|
if (currentIdArray.length > 0) {
|
|
27308
28111
|
idArraySignal.value = [];
|
|
28112
|
+
return true;
|
|
27309
28113
|
}
|
|
27310
|
-
return;
|
|
28114
|
+
return false;
|
|
27311
28115
|
}
|
|
27312
28116
|
const idArray = [];
|
|
27313
28117
|
let modified = false;
|
|
@@ -27329,7 +28133,9 @@ const createChildIdArrayUpdater = (childStore, childIdKey, idArraySignal) => {
|
|
|
27329
28133
|
}
|
|
27330
28134
|
if (modified || currentIdArray.length !== idArray.length) {
|
|
27331
28135
|
idArraySignal.value = idArray;
|
|
28136
|
+
return true;
|
|
27332
28137
|
}
|
|
28138
|
+
return false;
|
|
27333
28139
|
};
|
|
27334
28140
|
};
|
|
27335
28141
|
|
|
@@ -27747,6 +28553,24 @@ const TYPE_CONVERTERS = {
|
|
|
27747
28553
|
},
|
|
27748
28554
|
};
|
|
27749
28555
|
|
|
28556
|
+
/**
|
|
28557
|
+
* What a component can learn about the page it is rendered in, from the route
|
|
28558
|
+
* container rendering that page (see useContentKeepingLeavingPages in
|
|
28559
|
+
* route.jsx). `null` outside of any route container.
|
|
28560
|
+
*
|
|
28561
|
+
* - `isShown()` — whether the container would still show the page. False as
|
|
28562
|
+
* soon as the address leads elsewhere: the page is about to be taken down, or
|
|
28563
|
+
* hidden until the route transition leaving it is over, and nothing it reads
|
|
28564
|
+
* is worth a render any more. False for good once it is kept while leaving: a
|
|
28565
|
+
* return to its route mounts the page anew. Asked outside any render — by a
|
|
28566
|
+
* subscription deciding whether to re-render — and it reads the routes
|
|
28567
|
+
* untracked, so asking subscribes nobody to them.
|
|
28568
|
+
* - `leavingSignal` — true once the page is hidden and kept while its movement
|
|
28569
|
+
* plays: still mounted, so what it fills outside its own nodes (a slot in a
|
|
28570
|
+
* bar) steps out rather than showing with the page arriving.
|
|
28571
|
+
*/
|
|
28572
|
+
const RoutePageContext = createContext(null);
|
|
28573
|
+
|
|
27750
28574
|
const promiseStateWeakMap = new WeakMap();
|
|
27751
28575
|
const usePromiseAsyncData = (
|
|
27752
28576
|
promise,
|
|
@@ -27882,12 +28706,39 @@ const useActionAsyncData = (action, {
|
|
|
27882
28706
|
// has already processed the settlement and the detached DOM is discarded.
|
|
27883
28707
|
const runningState = action.runningStateSignal.peek();
|
|
27884
28708
|
const [, setTick] = useState(0);
|
|
28709
|
+
const routePage = useContext(RoutePageContext);
|
|
27885
28710
|
useEffect(() => {
|
|
28711
|
+
let stopOwingRender = null;
|
|
28712
|
+
const rerender = () => {
|
|
28713
|
+
// Not for a page the address has left: it is about to be taken down,
|
|
28714
|
+
// or hidden until the route transition leaving it is over, and its
|
|
28715
|
+
// route's action moving as that route stops matching would re-render
|
|
28716
|
+
// the whole page — inside the transition's update callback, where
|
|
28717
|
+
// every render delays the movement (see route_page_context.js).
|
|
28718
|
+
if (routePage && !routePage.isShown()) {
|
|
28719
|
+
// Owed rather than dropped: the address can come back before the
|
|
28720
|
+
// container renders (a redirect straight back), and the page then
|
|
28721
|
+
// stays on screen with what it read last.
|
|
28722
|
+
if (!stopOwingRender) {
|
|
28723
|
+
stopOwingRender = observeRouteRender(() => {
|
|
28724
|
+
if (routePage.isShown()) {
|
|
28725
|
+
rerender();
|
|
28726
|
+
}
|
|
28727
|
+
});
|
|
28728
|
+
}
|
|
28729
|
+
return;
|
|
28730
|
+
}
|
|
28731
|
+
if (stopOwingRender) {
|
|
28732
|
+
stopOwingRender();
|
|
28733
|
+
stopOwingRender = null;
|
|
28734
|
+
}
|
|
28735
|
+
setTick(n => n + 1);
|
|
28736
|
+
};
|
|
27886
28737
|
const unsubscribeFromRunningState = action.runningStateSignal.subscribe(state => {
|
|
27887
28738
|
if (state === RUNNING) {
|
|
27888
28739
|
dismissedActionWeakSet.delete(action);
|
|
27889
28740
|
}
|
|
27890
|
-
|
|
28741
|
+
rerender();
|
|
27891
28742
|
});
|
|
27892
28743
|
// The data does not come from this action's runs alone: dataSignal is a
|
|
27893
28744
|
// computed over the resource store, so an other action writing that store
|
|
@@ -27901,7 +28752,7 @@ const useActionAsyncData = (action, {
|
|
|
27901
28752
|
dataNotificationIsInitial = false;
|
|
27902
28753
|
return;
|
|
27903
28754
|
}
|
|
27904
|
-
|
|
28755
|
+
rerender();
|
|
27905
28756
|
});
|
|
27906
28757
|
// The params say WHICH question this is, and this hook reads them: to know
|
|
27907
28758
|
// there is nothing to ask for, and to start the run it owns. A binding
|
|
@@ -27914,7 +28765,7 @@ const useActionAsyncData = (action, {
|
|
|
27914
28765
|
paramsNotificationIsInitial = false;
|
|
27915
28766
|
return;
|
|
27916
28767
|
}
|
|
27917
|
-
|
|
28768
|
+
rerender();
|
|
27918
28769
|
});
|
|
27919
28770
|
// A debounced binding waits before it retargets, so nothing above changes
|
|
27920
28771
|
// while the delay runs — but what is on screen is already out of date.
|
|
@@ -27924,18 +28775,21 @@ const useActionAsyncData = (action, {
|
|
|
27924
28775
|
settlingNotificationIsInitial = false;
|
|
27925
28776
|
return;
|
|
27926
28777
|
}
|
|
27927
|
-
|
|
28778
|
+
rerender();
|
|
27928
28779
|
});
|
|
27929
28780
|
return () => {
|
|
27930
28781
|
unsubscribeFromRunningState();
|
|
27931
28782
|
unsubscribeFromData();
|
|
27932
28783
|
unsubscribeFromParams();
|
|
27933
28784
|
unsubscribeFromParamsSettling();
|
|
28785
|
+
if (stopOwingRender) {
|
|
28786
|
+
stopOwingRender();
|
|
28787
|
+
}
|
|
27934
28788
|
};
|
|
27935
28789
|
// Bound to the action, not to the mount: params given as a plain object
|
|
27936
28790
|
// make another action instance, and the component would otherwise stay
|
|
27937
28791
|
// subscribed to the state of the one it no longer reads.
|
|
27938
|
-
}, [action]);
|
|
28792
|
+
}, [action, routePage]);
|
|
27939
28793
|
|
|
27940
28794
|
// The params moved and the run has not started: under a debounce the action
|
|
27941
28795
|
// is still the previous one, COMPLETED with the previous answer. There is no
|
|
@@ -32095,13 +32949,6 @@ const keepLeavingPages = () => {
|
|
|
32095
32949
|
hold.containers.clear();
|
|
32096
32950
|
};
|
|
32097
32951
|
};
|
|
32098
|
-
|
|
32099
|
-
// What a page can learn about its place from the container rendering it:
|
|
32100
|
-
// whether it is a page kept while leaving (see keepLeavingPages). Such a page
|
|
32101
|
-
// is still mounted, and whatever it fills OUTSIDE its own nodes — a slot in a
|
|
32102
|
-
// bar, say — would otherwise go on showing with the page arriving (see
|
|
32103
|
-
// layout/slot.jsx).
|
|
32104
|
-
const RoutePageContext = createContext(null);
|
|
32105
32952
|
const LEAVING_ATTRIBUTE = "data-navi-route-leaving";
|
|
32106
32953
|
const css$13 = /* css */`[data-navi-route-leaving] {
|
|
32107
32954
|
display: none !important;
|
|
@@ -32226,7 +33073,21 @@ const RouteContainer = ({
|
|
|
32226
33073
|
shownBranchRef.current = activeBranch;
|
|
32227
33074
|
}
|
|
32228
33075
|
const branch = shownBranchRef.current || activeBranch;
|
|
32229
|
-
|
|
33076
|
+
// What this container would show if it rendered now, by the same rule as
|
|
33077
|
+
// the render above — asked from outside any render (see isShown below).
|
|
33078
|
+
const childrenRef = useRef(null);
|
|
33079
|
+
childrenRef.current = children;
|
|
33080
|
+
const readShownBranchRef = useRef(null);
|
|
33081
|
+
if (!readShownBranchRef.current) {
|
|
33082
|
+
readShownBranchRef.current = () => {
|
|
33083
|
+
if (routeRenderFrozen) {
|
|
33084
|
+
return shownBranchRef.current;
|
|
33085
|
+
}
|
|
33086
|
+
return collectBranches(childrenRef.current).activeBranch;
|
|
33087
|
+
};
|
|
33088
|
+
}
|
|
33089
|
+
const parentPage = useContext(RoutePageContext);
|
|
33090
|
+
const content = useContentKeepingLeavingPages(branch, readShownBranchRef.current, parentPage);
|
|
32230
33091
|
|
|
32231
33092
|
// The two things this component knows that nobody outside can find out: the
|
|
32232
33093
|
// pages a named fallback is the absence of — the children it was written
|
|
@@ -32263,7 +33124,7 @@ const RouteContainer = ({
|
|
|
32263
33124
|
// page being left must stay the child it was when a page is added beside it,
|
|
32264
33125
|
// or Preact takes it down and builds it again.
|
|
32265
33126
|
let leavingKeyCount = 0;
|
|
32266
|
-
const useContentKeepingLeavingPages = branch => {
|
|
33127
|
+
const useContentKeepingLeavingPages = (branch, readShownBranch, parentPage) => {
|
|
32267
33128
|
const [, rerender] = useReducer(countRenders, 0);
|
|
32268
33129
|
const shownRef = useRef(null);
|
|
32269
33130
|
const leavingRef = useRef([]);
|
|
@@ -32281,6 +33142,7 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
32281
33142
|
// of reach — the page arriving could not even take the focus — for as long
|
|
32282
33143
|
// as it is open, and one in a page kept while leaving stays open for the
|
|
32283
33144
|
// whole movement. The page is then taken down at once, as without one.
|
|
33145
|
+
shown.page.left = true;
|
|
32284
33146
|
leavingRef.current.push({
|
|
32285
33147
|
vnode: shown.vnode,
|
|
32286
33148
|
page: shown.page,
|
|
@@ -32297,10 +33159,11 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
32297
33159
|
let vnode = null;
|
|
32298
33160
|
if (branch) {
|
|
32299
33161
|
if (!page) {
|
|
32300
|
-
page =
|
|
32301
|
-
leavingSignal: signal(false)
|
|
32302
|
-
};
|
|
33162
|
+
page = createRoutePage(readShownBranch, parentPage);
|
|
32303
33163
|
}
|
|
33164
|
+
// A page carried over follows the branch it is now the page of.
|
|
33165
|
+
page.branchId = branch.id;
|
|
33166
|
+
page.element = element;
|
|
32304
33167
|
vnode = h(RoutePageContext.Provider, {
|
|
32305
33168
|
key,
|
|
32306
33169
|
value: page
|
|
@@ -32367,6 +33230,33 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
32367
33230
|
return children;
|
|
32368
33231
|
};
|
|
32369
33232
|
const countRenders = count => count + 1;
|
|
33233
|
+
// The page a container renders, as RoutePageContext hands it over (see
|
|
33234
|
+
// route_page_context.js). Shown while the container would pick its branch, or
|
|
33235
|
+
// a branch rendering the same element (it is then carried over), and while the
|
|
33236
|
+
// page it sits in is shown itself. Never again once kept while leaving: coming
|
|
33237
|
+
// back to its route mounts the page anew.
|
|
33238
|
+
const createRoutePage = (readShownBranch, parentPage) => {
|
|
33239
|
+
const page = {
|
|
33240
|
+
branchId: null,
|
|
33241
|
+
element: undefined,
|
|
33242
|
+
left: false,
|
|
33243
|
+
leavingSignal: signal(false),
|
|
33244
|
+
isShown: () => {
|
|
33245
|
+
if (page.left) {
|
|
33246
|
+
return false;
|
|
33247
|
+
}
|
|
33248
|
+
if (parentPage && !parentPage.isShown()) {
|
|
33249
|
+
return false;
|
|
33250
|
+
}
|
|
33251
|
+
const shownBranch = untracked(readShownBranch);
|
|
33252
|
+
if (!shownBranch) {
|
|
33253
|
+
return false;
|
|
33254
|
+
}
|
|
33255
|
+
return shownBranch.id === page.branchId || branchElement(shownBranch) === page.element;
|
|
33256
|
+
}
|
|
33257
|
+
};
|
|
33258
|
+
return page;
|
|
33259
|
+
};
|
|
32370
33260
|
// What Preact compares when a container changes page, below the <Route> it
|
|
32371
33261
|
// renders either way: the element the page is rendered with — a layout, for a
|
|
32372
33262
|
// container branch.
|
|
@@ -38706,887 +39596,351 @@ registerNaviCommand("--navi-nav-to", (source, event, { argument }) => {
|
|
|
38706
39596
|
console.warn(
|
|
38707
39597
|
`[navi] "--navi-nav-to" needs a destination: --navi-nav-to:/the/url (relative to the current page, or absolute).`,
|
|
38708
39598
|
);
|
|
38709
|
-
return undefined;
|
|
38710
|
-
}
|
|
38711
|
-
const target = resolveExplicitTarget(source) || source;
|
|
38712
|
-
return {
|
|
38713
|
-
target,
|
|
38714
|
-
// Which way there is worn by the source, as a link wears it (see
|
|
38715
|
-
// link_replace.js): `<Button command="--navi-nav-to:/done" replace>`.
|
|
38716
|
-
implementation: () =>
|
|
38717
|
-
navTo(
|
|
38718
|
-
argument,
|
|
38719
|
-
linkAsksForReplace(source) ? { replace: true } : undefined,
|
|
38720
|
-
),
|
|
38721
|
-
};
|
|
38722
|
-
});
|
|
38723
|
-
|
|
38724
|
-
// Back to the screen the reader came from, with somewhere to land when there
|
|
38725
|
-
// is none of ours behind — a url opened cold (see navBack). The fallback is
|
|
38726
|
-
// the argument for the reason --navi-nav-to's destination is: it says WHAT the
|
|
38727
|
-
// command does when there is nothing to go back to.
|
|
38728
|
-
registerNaviCommand("--navi-nav-back", (source, event, { argument }) => {
|
|
38729
|
-
const target = resolveExplicitTarget(source) || source;
|
|
38730
|
-
return {
|
|
38731
|
-
target,
|
|
38732
|
-
implementation: () => navBack({ fallback: argument }),
|
|
38733
|
-
};
|
|
38734
|
-
});
|
|
38735
|
-
|
|
38736
|
-
registerNaviCommand(
|
|
38737
|
-
"--navi-toggle",
|
|
38738
|
-
(source, event, { anchor, value } = {}) => {
|
|
38739
|
-
const target =
|
|
38740
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38741
|
-
if (!target) {
|
|
38742
|
-
return undefined;
|
|
38743
|
-
}
|
|
38744
|
-
return {
|
|
38745
|
-
target,
|
|
38746
|
-
implementation: () => {
|
|
38747
|
-
const isExpanded = target.getAttribute("aria-expanded") === "true";
|
|
38748
|
-
const customEventName = isExpanded
|
|
38749
|
-
? "navi_request_close"
|
|
38750
|
-
: "navi_request_open";
|
|
38751
|
-
return dispatchCustomEvent(target, customEventName, {
|
|
38752
|
-
event,
|
|
38753
|
-
source: resolveCommandProxySource(source),
|
|
38754
|
-
anchor,
|
|
38755
|
-
// Same as --navi-open below: the half of the toggle that opens says
|
|
38756
|
-
// what it opens ON, and the half that closes carries it too so the
|
|
38757
|
-
// popup never has to ask which half it just heard.
|
|
38758
|
-
value:
|
|
38759
|
-
value === undefined ? resolveCommandValue(source, event) : value,
|
|
38760
|
-
});
|
|
38761
|
-
},
|
|
38762
|
-
};
|
|
38763
|
-
},
|
|
38764
|
-
);
|
|
38765
|
-
// A popup that edits opens ON something, and the press is the only place that
|
|
38766
|
-
// knows which one. It says it with `value` — "what this is about", as
|
|
38767
|
-
// everywhere else — rather than with an argument, which says what the command
|
|
38768
|
-
// DOES: "open" is already a complete instruction, unlike --navi-go-to-slide.
|
|
38769
|
-
// <Button value={radar.id} command="--navi-open" commandfor="radar-dialog">
|
|
38770
|
-
// The popup is told before it opens — see Dialog/Popover's `onOpen`.
|
|
38771
|
-
registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
|
|
38772
|
-
const target =
|
|
38773
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38774
|
-
if (!target) {
|
|
38775
|
-
return undefined;
|
|
38776
|
-
}
|
|
38777
|
-
return {
|
|
38778
|
-
target,
|
|
38779
|
-
implementation: () => {
|
|
38780
|
-
// The popup reads `anchor` first and falls back to the source (see
|
|
38781
|
-
// onnavi_request_open in popover.jsx): without an `anchor`, the element
|
|
38782
|
-
// asking is the element opened against.
|
|
38783
|
-
return dispatchCustomEvent(target, "navi_request_open", {
|
|
38784
|
-
event,
|
|
38785
|
-
source: resolveCommandProxySource(source),
|
|
38786
|
-
anchor,
|
|
38787
|
-
value: value === undefined ? resolveCommandValue(source, event) : value,
|
|
38788
|
-
});
|
|
38789
|
-
},
|
|
38790
|
-
};
|
|
38791
|
-
});
|
|
38792
|
-
/**
|
|
38793
|
-
* Tells the popup a source opens that a press on it has started, ahead of the
|
|
38794
|
-
* release that runs the command: what the opening will read can be asked for
|
|
38795
|
-
* now (see open_controller.js). A press rather than a hover, because what
|
|
38796
|
-
* that reads is keyed by the popup — one sheet per card — and a pointer
|
|
38797
|
-
* crossing a list of cards has chosen none of them; on a touch screen the
|
|
38798
|
-
* press is the first thing the finger says anyway.
|
|
38799
|
-
*
|
|
38800
|
-
* The target is found the way --navi-open finds it, but silently: nothing
|
|
38801
|
-
* runs here, and a source with no target says so at the release.
|
|
38802
|
-
*/
|
|
38803
|
-
const announceOpeningPress = (source, command) => {
|
|
38804
|
-
if (command !== "--navi-open" && command !== "--navi-toggle") {
|
|
38805
|
-
return;
|
|
38806
|
-
}
|
|
38807
|
-
const commandFor = source.getAttribute("commandfor");
|
|
38808
|
-
const target = commandFor
|
|
38809
|
-
? document.getElementById(commandFor)
|
|
38810
|
-
: resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38811
|
-
if (!target) {
|
|
38812
|
-
return;
|
|
38813
|
-
}
|
|
38814
|
-
dispatchCustomEvent(target, "navi_open_press");
|
|
38815
|
-
};
|
|
38816
|
-
// "--navi-close:all" closes every expandable above the source, nearest first —
|
|
38817
|
-
// a link leaving from a badge shown over a sheet leaves both. A surface that
|
|
38818
|
-
// refuses (a form asking about its changes) keeps what is above it open too:
|
|
38819
|
-
// one cannot be out of the sheet while still in the badge.
|
|
38820
|
-
registerNaviCommand("--navi-close", (source, event, { argument }) => {
|
|
38821
|
-
const target =
|
|
38822
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38823
|
-
if (!target) {
|
|
38824
|
-
return undefined;
|
|
38825
|
-
}
|
|
38826
|
-
return {
|
|
38827
|
-
target,
|
|
38828
|
-
implementation: () => {
|
|
38829
|
-
const detail = { event, source: resolveCommandProxySource(source) };
|
|
38830
|
-
if (argument === "all") {
|
|
38831
|
-
return requestCloseUpward(target, detail);
|
|
38832
|
-
}
|
|
38833
|
-
return dispatchCustomEvent(target, "navi_request_close", detail);
|
|
38834
|
-
},
|
|
38835
|
-
};
|
|
38836
|
-
});
|
|
38837
|
-
const requestCloseUpward = (target, detail) => {
|
|
38838
|
-
let expandable = target;
|
|
38839
|
-
while (expandable) {
|
|
38840
|
-
const closing = dispatchCustomEvent(
|
|
38841
|
-
expandable,
|
|
38842
|
-
"navi_request_close",
|
|
38843
|
-
detail,
|
|
38844
|
-
);
|
|
38845
|
-
if (!closing) {
|
|
38846
|
-
return false;
|
|
38847
|
-
}
|
|
38848
|
-
const parent = expandable.parentElement;
|
|
38849
|
-
expandable = parent ? parent.closest("[aria-expanded]") : null;
|
|
38850
|
-
}
|
|
38851
|
-
return true;
|
|
38852
|
-
};
|
|
38853
|
-
registerNaviCommand("--navi-cancel", (source, event) => {
|
|
38854
|
-
const target =
|
|
38855
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38856
|
-
if (!target) {
|
|
38857
|
-
return undefined;
|
|
38858
|
-
}
|
|
38859
|
-
return {
|
|
38860
|
-
target,
|
|
38861
|
-
implementation: () => {
|
|
38862
|
-
return dispatchCustomEvent(target, "navi_request_close", {
|
|
38863
|
-
event,
|
|
38864
|
-
source: resolveCommandProxySource(source),
|
|
38865
|
-
isCancel: true,
|
|
38866
|
-
});
|
|
38867
|
-
},
|
|
38868
|
-
};
|
|
38869
|
-
});
|
|
38870
|
-
// "Yes, do it" — said inside the popup of a confirm picker (see
|
|
38871
|
-
// picker/picker_confirm.jsx), by its own yes button or by whatever a caller put
|
|
38872
|
-
// there instead. Its counterpart is --navi-cancel; the difference between the
|
|
38873
|
-
// two is this event, since both then close the popup, and a popup that closes
|
|
38874
|
-
// without having said this is the answer "no".
|
|
38875
|
-
registerNaviCommand("--navi-confirm", (source, event) => {
|
|
38876
|
-
const target =
|
|
38877
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38878
|
-
if (!target) {
|
|
38879
|
-
return undefined;
|
|
38880
|
-
}
|
|
38881
|
-
return {
|
|
38882
|
-
target,
|
|
38883
|
-
implementation: () => {
|
|
38884
|
-
const commandSource = resolveCommandProxySource(source);
|
|
38885
|
-
dispatchCustomEvent(target, "navi_request_confirm", {
|
|
38886
|
-
event,
|
|
38887
|
-
source: commandSource,
|
|
38888
|
-
});
|
|
38889
|
-
return dispatchCustomEvent(target, "navi_request_close", {
|
|
38890
|
-
event,
|
|
38891
|
-
source: commandSource,
|
|
38892
|
-
});
|
|
38893
|
-
},
|
|
38894
|
-
};
|
|
38895
|
-
});
|
|
38896
|
-
registerNaviCommand("--navi-define", (source, event) => {
|
|
38897
|
-
const target =
|
|
38898
|
-
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
38899
|
-
if (!target) {
|
|
38900
|
-
return undefined;
|
|
38901
|
-
}
|
|
38902
|
-
return {
|
|
38903
|
-
target,
|
|
38904
|
-
implementation: () => executeNaviDefine(source, event, target),
|
|
38905
|
-
};
|
|
38906
|
-
});
|
|
38907
|
-
// A send whose target is the popup itself: what is around the source IS the
|
|
38908
|
-
// popup's answer, and a picker runs its action with that answer as it closes
|
|
38909
|
-
// (see onRequestClose in picker_custom.jsx) — dispatching the action here too
|
|
38910
|
-
// would run it twice. `isSend` tells this close from a dismissal (the cross,
|
|
38911
|
-
// a press outside, Escape): the person pressed a choice, which a picker may
|
|
38912
|
-
// act on even when the choice is the one it already held.
|
|
38913
|
-
const executeNaviDefine = (source, event, target) => {
|
|
38914
|
-
return dispatchCustomEvent(target, "navi_request_close", {
|
|
38915
|
-
event,
|
|
38916
|
-
source: resolveCommandProxySource(source),
|
|
38917
|
-
isSend: true,
|
|
38918
|
-
});
|
|
38919
|
-
};
|
|
38920
|
-
|
|
38921
|
-
registerNaviCommand("--navi-scroll", (source, event) => {
|
|
38922
|
-
const target =
|
|
38923
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38924
|
-
if (!target) {
|
|
38925
|
-
return undefined;
|
|
38926
|
-
}
|
|
38927
|
-
return {
|
|
38928
|
-
target,
|
|
38929
|
-
implementation: () => {
|
|
38930
|
-
return dispatchCustomEvent(target, "navi_request_scroll", {
|
|
38931
|
-
event,
|
|
38932
|
-
id: resolveCommandValue(source, event),
|
|
38933
|
-
});
|
|
38934
|
-
},
|
|
38935
|
-
};
|
|
38936
|
-
});
|
|
38937
|
-
// The press a hand is still making, given to something that can be carried: a
|
|
38938
|
-
// hold opened a popup, and what the popup holds is to follow that same finger
|
|
38939
|
-
// without it lifting and pressing again. `event` is what the press set off (the
|
|
38940
|
-
// popup's open event), and the press is read from its chain. Run once the popup
|
|
38941
|
-
// has settled, which the caller cannot see: the carry measures its element and
|
|
38942
|
-
// puts a copy in the top layer, neither of which holds while a transition is
|
|
38943
|
-
// still moving the box (see afterPopupLift). What the carry then is — its
|
|
38944
|
-
// outcomes, its refusal — is the element's (see interaction_drag.js).
|
|
38945
|
-
registerNaviCommand("--navi-grab", (source, event) => {
|
|
38946
|
-
const target =
|
|
38947
|
-
resolveExplicitTarget(source) || source.closest("[data-drag-source]");
|
|
38948
|
-
if (!target) {
|
|
38949
|
-
return undefined;
|
|
38950
|
-
}
|
|
38951
|
-
return {
|
|
38952
|
-
target,
|
|
38953
|
-
implementation: () => {
|
|
38954
|
-
afterPopupLift(() => {
|
|
38955
|
-
// Taken away with the popup before it settled.
|
|
38956
|
-
if (!target.isConnected) {
|
|
38957
|
-
return;
|
|
38958
|
-
}
|
|
38959
|
-
dispatchCustomEvent(target, "navi_request_grab", { event });
|
|
38960
|
-
});
|
|
38961
|
-
return true;
|
|
38962
|
-
},
|
|
38963
|
-
};
|
|
38964
|
-
});
|
|
38965
|
-
registerNaviCommand("--navi-check", (source, event) => {
|
|
38966
|
-
const target =
|
|
38967
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38968
|
-
if (!target) {
|
|
38969
|
-
return undefined;
|
|
38970
|
-
}
|
|
38971
|
-
return {
|
|
38972
|
-
target,
|
|
38973
|
-
implementation: () => {
|
|
38974
|
-
return dispatchCustomEvent(target, "navi_request_check", {
|
|
38975
|
-
event,
|
|
38976
|
-
});
|
|
38977
|
-
},
|
|
38978
|
-
};
|
|
38979
|
-
});
|
|
38980
|
-
registerNaviCommand("--navi-uncheck", (source, event) => {
|
|
38981
|
-
const target =
|
|
38982
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38983
|
-
if (!target) {
|
|
38984
|
-
return undefined;
|
|
38985
|
-
}
|
|
38986
|
-
return {
|
|
38987
|
-
target,
|
|
38988
|
-
implementation: () => {
|
|
38989
|
-
return dispatchCustomEvent(target, "navi_request_uncheck", {
|
|
38990
|
-
event,
|
|
38991
|
-
});
|
|
38992
|
-
},
|
|
38993
|
-
};
|
|
38994
|
-
});
|
|
38995
|
-
registerNaviCommand("--navi-select", (source, event) => {
|
|
38996
|
-
const target =
|
|
38997
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
38998
|
-
if (!target) {
|
|
38999
|
-
return undefined;
|
|
39000
|
-
}
|
|
39001
|
-
return {
|
|
39002
|
-
target,
|
|
39003
|
-
implementation: () => {
|
|
39004
|
-
return dispatchCustomEvent(target, "navi_request_select", {
|
|
39005
|
-
event,
|
|
39006
|
-
id: resolveCommandValue(source, event),
|
|
39007
|
-
});
|
|
39008
|
-
},
|
|
39009
|
-
};
|
|
39010
|
-
});
|
|
39011
|
-
registerNaviCommand("--navi-unselect", (source, event) => {
|
|
39012
|
-
const target =
|
|
39013
|
-
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39014
|
-
if (!target) {
|
|
39015
|
-
return undefined;
|
|
39016
|
-
}
|
|
39017
|
-
return {
|
|
39018
|
-
target,
|
|
39019
|
-
implementation: () => {
|
|
39020
|
-
return dispatchCustomEvent(target, "navi_request_unselect", {
|
|
39021
|
-
event,
|
|
39022
|
-
id: resolveCommandValue(source, event),
|
|
39023
|
-
});
|
|
39024
|
-
},
|
|
39025
|
-
};
|
|
39026
|
-
});
|
|
39027
|
-
|
|
39028
|
-
/**
|
|
39029
|
-
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
39030
|
-
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
39031
|
-
* mounted run inside the call too, exactly as they would on any other commit.
|
|
39032
|
-
*
|
|
39033
|
-
* For the caller that has to read the DOM it just asked for — measuring an
|
|
39034
|
-
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
39035
|
-
* to do it, because what comes after is a browser event still in flight
|
|
39036
|
-
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
39037
|
-
*
|
|
39038
|
-
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
39039
|
-
* render queue drains; swapping it for "right now" for the duration of the call
|
|
39040
|
-
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
39041
|
-
* case above: rendering synchronously in the middle of an event gives up the
|
|
39042
|
-
* batching that makes several state changes one commit.
|
|
39043
|
-
*/
|
|
39044
|
-
const flushSyncRendering = (fn) => {
|
|
39045
|
-
const debounceRenderingPrevious = options.debounceRendering;
|
|
39046
|
-
options.debounceRendering = (drainRenderQueue) => {
|
|
39047
|
-
drainRenderQueue();
|
|
39048
|
-
};
|
|
39049
|
-
try {
|
|
39050
|
-
fn();
|
|
39051
|
-
} finally {
|
|
39052
|
-
options.debounceRendering = debounceRenderingPrevious;
|
|
39053
|
-
}
|
|
39054
|
-
};
|
|
39055
|
-
|
|
39056
|
-
/**
|
|
39057
|
-
* Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
|
|
39058
|
-
* (non-top-layer) renderers — operate on a plain DOM element, no knowledge
|
|
39059
|
-
* of which of the two owns it.
|
|
39060
|
-
*/
|
|
39061
|
-
|
|
39062
|
-
|
|
39063
|
-
/**
|
|
39064
|
-
* Whether a visibleRectEffect delivery is one that can have taken height away
|
|
39065
|
-
* from a popup, and so pushed whatever holds focus out of sight:
|
|
39066
|
-
* - "resize": the window/visual viewport settled — which is also how the
|
|
39067
|
-
* on-screen keyboard arrives, overlay or not (window_size.js);
|
|
39068
|
-
* - ELEMENT_SIZE_CHANGE: the popup's own box measured different;
|
|
39069
|
-
* - "focusin": the focus-settled re-measure, for the room that changes with
|
|
39070
|
-
* nothing announcing it (subscribeFocusSettled in window_size.js).
|
|
39071
|
-
*
|
|
39072
|
-
* Everything else is a scroll of one kind or another, where nothing got
|
|
39073
|
-
* smaller and scrolling the focused element back would fight the very gesture
|
|
39074
|
-
* that fired it.
|
|
39075
|
-
*/
|
|
39076
|
-
const mayHaveHiddenFocus = (event) => {
|
|
39077
|
-
const type = event?.type;
|
|
39078
|
-
return (
|
|
39079
|
-
type === "resize" || type === ELEMENT_SIZE_CHANGE || type === "focusin"
|
|
39080
|
-
);
|
|
39081
|
-
};
|
|
39082
|
-
|
|
39083
|
-
const scrollportHeightMap = new WeakMap();
|
|
39084
|
-
/**
|
|
39085
|
-
* Scrolls whatever holds focus inside `popupEl` back into view, if the popup
|
|
39086
|
-
* getting shorter has pushed it out.
|
|
39087
|
-
*
|
|
39088
|
-
* The case this exists for: a field low in the scrolling body of a popup that
|
|
39089
|
-
* also has a footer (box.jsx — with a body, the body is the only thing that
|
|
39090
|
-
* scrolls and the footer is a sibling sitting right under it). Focusing the
|
|
39091
|
-
* field makes the browser scroll it into view, which it does against the
|
|
39092
|
-
* popup's height AT THAT MOMENT; the on-screen keyboard then opens and takes
|
|
39093
|
-
* that height away. The body shrinks, its scrollTop does not move, so the
|
|
39094
|
-
* content slides down relative to the shorter scrollport and the field ends up
|
|
39095
|
-
* past its bottom edge — visually, swallowed by the footer. The browser does
|
|
39096
|
-
* not redo a scroll-into-view it already answered, so this does.
|
|
39097
|
-
*
|
|
39098
|
-
* Only what the shrink itself hid, though: with the keyboard up the user reads
|
|
39099
|
-
* the rest of the popup by scrolling the field away — to reach the submit under
|
|
39100
|
-
* it, typically — and the room keeps changing while they do (a keyboard settling
|
|
39101
|
-
* in two steps, a suggestion strip, a browser bar). Answering each of those by
|
|
39102
|
-
* scrolling the field back takes the popup away from wherever they had just
|
|
39103
|
-
* scrolled it, over and over: what they were reading cannot be reached at all
|
|
39104
|
-
* without blurring the field first, and the popup reads as unscrollable. So the
|
|
39105
|
-
* field is brought back only when it was in view before the room shrank, which
|
|
39106
|
-
* one remembered number answers: a resize moves neither scrollTop nor the
|
|
39107
|
-
* element's offset inside the scrolled content, so measuring against the height
|
|
39108
|
-
* the scrollport HAD is measuring the state before the change.
|
|
39109
|
-
*
|
|
39110
|
-
* Scoped to the field's own scroll container (never the page): a popup traps
|
|
39111
|
-
* scrolling precisely so the document underneath cannot move, and a plain
|
|
39112
|
-
* scrollIntoView walks past a container whose scrollbar isn't visible — see
|
|
39113
|
-
* scrollIntoViewScoped's own doc.
|
|
39114
|
-
*
|
|
39115
|
-
* "nearest": the smallest scroll that makes it visible, and none at all when it
|
|
39116
|
-
* already is. Where it lands is the container's own business — a navi scroller
|
|
39117
|
-
* keeps a band free at its edges so a field never comes back glued to one (see
|
|
39118
|
-
* scroll-padding in box.jsx).
|
|
39119
|
-
*/
|
|
39120
|
-
const keepFocusedElementVisible = (popupEl) => {
|
|
39121
|
-
const { activeElement } = document;
|
|
39122
|
-
if (!activeElement || activeElement === popupEl) {
|
|
39123
|
-
return;
|
|
39124
|
-
}
|
|
39125
|
-
if (!popupEl.contains(activeElement)) {
|
|
39126
|
-
return;
|
|
39127
|
-
}
|
|
39128
|
-
const scrollContainer = getScrollContainer(activeElement);
|
|
39129
|
-
if (!scrollContainer || !popupEl.contains(scrollContainer)) {
|
|
39130
|
-
// What scrolls the field is outside the popup, which means the page: a
|
|
39131
|
-
// popup holds it still on purpose (trapScrollInside), so there is nothing
|
|
39132
|
-
// here to scroll back.
|
|
39133
|
-
return;
|
|
39134
|
-
}
|
|
39135
|
-
const scrollportHeight = scrollContainer.clientHeight;
|
|
39136
|
-
const scrollportHeightBefore = scrollportHeightMap.get(scrollContainer);
|
|
39137
|
-
scrollportHeightMap.set(scrollContainer, scrollportHeight);
|
|
39138
|
-
if (scrollportHeightBefore !== undefined) {
|
|
39139
|
-
if (scrollportHeight >= scrollportHeightBefore) {
|
|
39140
|
-
// Nothing was taken away, so nothing was hidden by this.
|
|
39141
|
-
return;
|
|
39142
|
-
}
|
|
39143
|
-
if (
|
|
39144
|
-
!isVisibleInScrollport(
|
|
39145
|
-
activeElement,
|
|
39146
|
-
scrollContainer,
|
|
39147
|
-
scrollportHeightBefore,
|
|
39148
|
-
)
|
|
39149
|
-
) {
|
|
39150
|
-
return;
|
|
39151
|
-
}
|
|
39152
|
-
}
|
|
39153
|
-
scrollIntoViewScoped(activeElement, {
|
|
39154
|
-
container: scrollContainer,
|
|
39155
|
-
block: "nearest",
|
|
39156
|
-
});
|
|
39157
|
-
};
|
|
39158
|
-
|
|
39159
|
-
// Whether any part of `el` was showing in `container` back when its scrollport
|
|
39160
|
-
// was `scrollportHeight` tall. Both boxes are read now: the container may have
|
|
39161
|
-
// moved as well as shrunk (a centered dialog re-centers itself), and the
|
|
39162
|
-
// difference between the two tops is what that move leaves alone.
|
|
39163
|
-
const isVisibleInScrollport = (el, container, scrollportHeight) => {
|
|
39164
|
-
const elRect = el.getBoundingClientRect();
|
|
39165
|
-
const containerRect = container.getBoundingClientRect();
|
|
39166
|
-
const elTop = elRect.top - containerRect.top + container.scrollTop;
|
|
39167
|
-
const elBottom = elTop + elRect.height;
|
|
39168
|
-
const scrollportTop = container.scrollTop;
|
|
39169
|
-
const scrollportBottom = scrollportTop + scrollportHeight;
|
|
39170
|
-
return elBottom > scrollportTop && elTop < scrollportBottom;
|
|
39171
|
-
};
|
|
39172
|
-
|
|
39173
|
-
/**
|
|
39174
|
-
* Calls `onSettled` once `el`'s current CSS transition is over — via
|
|
39175
|
-
* `transitionend`, with a safety `setTimeout` fallback matching the longest
|
|
39176
|
-
* `transition-duration`, in case nothing actually transitions or an event is
|
|
39177
|
-
* missed.
|
|
39178
|
-
*
|
|
39179
|
-
* Returns a "cancel" function, so a caller whose instance has been superseded
|
|
39180
|
-
* (a fresh open/close about to set its own state) can keep this stale one from
|
|
39181
|
-
* firing later. Cancelling only stops `onSettled`: undoing whatever the caller
|
|
39182
|
-
* did up front is that fresh call's business, not this one's.
|
|
39183
|
-
*/
|
|
39184
|
-
const whenTransitionSettles = (el, onSettled) => {
|
|
39185
|
-
let settled = false;
|
|
39186
|
-
const onTransitionEnd = (transitionEvent) => {
|
|
39187
|
-
if (transitionEvent.target === el) {
|
|
39188
|
-
finish();
|
|
39189
|
-
}
|
|
39190
|
-
};
|
|
39191
|
-
const stopWatching = () => {
|
|
39192
|
-
settled = true;
|
|
39193
|
-
el.removeEventListener("transitionend", onTransitionEnd);
|
|
39194
|
-
clearTimeout(safetyTimeoutId);
|
|
39195
|
-
};
|
|
39196
|
-
const finish = () => {
|
|
39197
|
-
if (settled) {
|
|
39198
|
-
return;
|
|
39199
|
-
}
|
|
39200
|
-
stopWatching();
|
|
39201
|
-
onSettled();
|
|
39202
|
-
};
|
|
39203
|
-
el.addEventListener("transitionend", onTransitionEnd);
|
|
39204
|
-
const durationsInSeconds = getComputedStyle(el)
|
|
39205
|
-
.transitionDuration.split(",")
|
|
39206
|
-
.map((value) => parseFloat(value) || 0);
|
|
39207
|
-
const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
|
|
39208
|
-
const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
|
|
39209
|
-
return () => {
|
|
39210
|
-
if (settled) {
|
|
39211
|
-
return;
|
|
39212
|
-
}
|
|
39213
|
-
stopWatching();
|
|
39214
|
-
};
|
|
39215
|
-
};
|
|
39216
|
-
|
|
39217
|
-
/**
|
|
39218
|
-
* Drops the document's text selection when it lives inside `el`, leaving a
|
|
39219
|
-
* selection made elsewhere on the page alone.
|
|
39220
|
-
*
|
|
39221
|
-
* A popup being closed takes its content with it, and a selection is a claim
|
|
39222
|
-
* on content the user can still act on: kept, it would outlive the surface it
|
|
39223
|
-
* was made on — painted on the box for the length of its exit transition, with
|
|
39224
|
-
* the handles and the copy toolbar a phone draws from the live selection
|
|
39225
|
-
* hanging over something that is going away — and reappear with the box on
|
|
39226
|
-
* the next opening. Dropping the selection is the only way to remove that
|
|
39227
|
-
* chrome: a `user-select: none` on the closing box hides the highlight in some
|
|
39228
|
-
* browsers only, and the handles are the browser's own, drawn from the
|
|
39229
|
-
* selection object rather than from any style.
|
|
39230
|
-
*/
|
|
39231
|
-
const clearTextSelectionInside = (el) => {
|
|
39232
|
-
const selection = window.getSelection();
|
|
39233
|
-
if (!selection || selection.rangeCount === 0) {
|
|
39234
|
-
return;
|
|
39235
|
-
}
|
|
39236
|
-
const range = selection.getRangeAt(0);
|
|
39237
|
-
if (!el.contains(range.commonAncestorContainer)) {
|
|
39238
|
-
return;
|
|
39239
|
-
}
|
|
39240
|
-
selection.removeAllRanges();
|
|
39241
|
-
};
|
|
39242
|
-
|
|
39243
|
-
/**
|
|
39244
|
-
* Disables pointer-events on `el` until its current CSS transition settles —
|
|
39245
|
-
* avoids the cursor changing/something becoming clickable while the popup is
|
|
39246
|
-
* still visually moving into or out of place.
|
|
39247
|
-
*
|
|
39248
|
-
* Returns whenTransitionSettles' own "cancel" function: it doesn't restore
|
|
39249
|
-
* pointer-events, since a fresh call for the next open/close is about to set
|
|
39250
|
-
* its own state.
|
|
39251
|
-
*/
|
|
39252
|
-
const suppressPointerEventsDuringTransition = (el) => {
|
|
39253
|
-
el.style.pointerEvents = "none";
|
|
39254
|
-
return whenTransitionSettles(el, () => {
|
|
39255
|
-
el.style.pointerEvents = "";
|
|
39256
|
-
});
|
|
39257
|
-
};
|
|
39258
|
-
|
|
39259
|
-
/**
|
|
39260
|
-
* Hides the backdrop, deferring until the browser's matching "click" fires
|
|
39261
|
-
* when `closeEvent` was triggered by a mousedown (see popover.jsx's top
|
|
39262
|
-
* comment for why) — same capture-phase-on-document pattern as
|
|
39263
|
-
* armSuppressNextOpenRequest in open_controller.js, which a plain timeout
|
|
39264
|
-
* can't safely replace: mouseup (and the click that follows it) can land an
|
|
39265
|
-
* arbitrarily long time after mousedown (the user is still holding the
|
|
39266
|
-
* button down), so a short timeout can fire first and hide the backdrop
|
|
39267
|
-
* before its own click ever arrives. A capture-phase listener on document
|
|
39268
|
-
* fires for every click regardless of what any bubble-phase handler does
|
|
39269
|
-
* downstream, so no fallback timer is needed.
|
|
39270
|
-
*
|
|
39271
|
-
* `hide` is the caller's own way to actually hide the backdrop
|
|
39272
|
-
* (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
|
|
39273
|
-
* for a plain div) — this helper only owns the mousedown/click timing.
|
|
39274
|
-
*
|
|
39275
|
-
* Returns a disarm function (or undefined if hidden immediately), so a
|
|
39276
|
-
* fresh open can cancel a pending hide it's about to make redundant.
|
|
39277
|
-
*/
|
|
39278
|
-
const armPointerDownOutsideClose = (closeEvent, hide) => {
|
|
39279
|
-
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
39280
|
-
if (!mousedownEvent) {
|
|
39281
|
-
hide();
|
|
39282
|
-
return undefined;
|
|
39283
|
-
}
|
|
39284
|
-
const onClick = () => {
|
|
39285
|
-
document.removeEventListener("click", onClick, { capture: true });
|
|
39286
|
-
hide();
|
|
39287
|
-
};
|
|
39288
|
-
document.addEventListener("click", onClick, { capture: true });
|
|
39289
|
-
return () => {
|
|
39290
|
-
document.removeEventListener("click", onClick, { capture: true });
|
|
39291
|
-
};
|
|
39292
|
-
};
|
|
39293
|
-
|
|
39294
|
-
/**
|
|
39295
|
-
* Maps a positionArea y/x pair to a concrete `navi-animation` value (a
|
|
39296
|
-
* `prefix` plus a direction word), or `null` if both axes overlap the anchor
|
|
39297
|
-
* (no direction at all — that's `resolvedAnimationKind === "scaling"`
|
|
39298
|
-
* territory instead, see resolveAutoAnimationKind below).
|
|
39299
|
-
*
|
|
39300
|
-
* `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
|
|
39301
|
-
* when docked) keeps the word as the compass direction the popup comes
|
|
39302
|
-
* from: placed "top" (a point/corner), it slides in from the top.
|
|
39303
|
-
* `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
|
|
39304
|
-
* direction instead, the opposite compass point: placed "top" of the
|
|
39305
|
-
* anchor, it moves/grows up, away from the anchor (which sits below it).
|
|
39306
|
-
*
|
|
39307
|
-
* "inset-*"/"center" contribute no direction on their axis either way.
|
|
39308
|
-
*/
|
|
39309
|
-
const resolveDirectionValue = (y, x, { prefix }) => {
|
|
39310
|
-
const yWord =
|
|
39311
|
-
y === "top"
|
|
39312
|
-
? prefix === "expand"
|
|
39313
|
-
? "up"
|
|
39314
|
-
: "top"
|
|
39315
|
-
: y === "bottom"
|
|
39316
|
-
? prefix === "expand"
|
|
39317
|
-
? "down"
|
|
39318
|
-
: "bottom"
|
|
39319
|
-
: null;
|
|
39320
|
-
const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
|
|
39321
|
-
if (!yWord && !xWord) {
|
|
39322
|
-
return null;
|
|
39323
|
-
}
|
|
39324
|
-
return yWord && xWord
|
|
39325
|
-
? `${prefix}-${yWord}-${xWord}`
|
|
39326
|
-
: `${prefix}-${yWord || xWord}`;
|
|
39327
|
-
};
|
|
39328
|
-
|
|
39329
|
-
/**
|
|
39330
|
-
* Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
|
|
39331
|
-
* — picked for any real anchor, or for a point/corner placed dead-center
|
|
39332
|
-
* (both positionArea axes overlapping — there's no sensible direction to
|
|
39333
|
-
* slide from in that case). "sliding" otherwise. `anchor` is `undefined`
|
|
39334
|
-
* for any no-anchor/docked case (Dialog always, Popover's own custom
|
|
39335
|
-
* renderer when there's no real anchor), so this collapses to "scaling"
|
|
39336
|
-
* there only for the dead-center case, "sliding" otherwise. The two
|
|
39337
|
-
* "overlapping" booleans below describe the *positionArea* itself (a bare
|
|
39338
|
-
* word vs. "inset-"/"center"), not anything about the anchor — they'd
|
|
39339
|
-
* mean exactly the same thing even with no anchor at all, since it's the
|
|
39340
|
-
* position strategy, not the anchor, that decides whether there's a
|
|
39341
|
-
* direction to slide from.
|
|
39342
|
-
*/
|
|
39343
|
-
const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
|
|
39344
|
-
const yIsOverlapping =
|
|
39345
|
-
parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
|
|
39346
|
-
const xIsOverlapping =
|
|
39347
|
-
parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
|
|
39348
|
-
return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
|
|
39349
|
-
};
|
|
39350
|
-
|
|
39351
|
-
/**
|
|
39352
|
-
* An opening that reaches a popup with no element of its own to show.
|
|
39353
|
-
*
|
|
39354
|
-
* The one cause seen so far, and it is silent: something inside the popup
|
|
39355
|
-
* suspended while its content was being built, and the `<Loading>` that caught
|
|
39356
|
-
* it sits ABOVE the popup. A boundary suspending on an update keeps the page as
|
|
39357
|
-
* it was, as a copy, so what is opened here belongs to a copy nobody owns any
|
|
39358
|
-
* more — and once the data arrives the subtree is rebuilt from scratch, closed
|
|
39359
|
-
* and empty. Nothing is thrown, nothing is drawn, and the press is lost.
|
|
39360
|
-
*/
|
|
39361
|
-
const warnPopupHasNoElementToOpen = (popupKind) => {
|
|
39362
|
-
console.warn(
|
|
39363
|
-
`[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 }).`,
|
|
39364
|
-
);
|
|
39365
|
-
};
|
|
39366
|
-
|
|
39367
|
-
/**
|
|
39368
|
-
* A popup reads "outside" from its own border box: what a press lands on
|
|
39369
|
-
* decides nothing on its own (a genuine backdrop press and a press on the
|
|
39370
|
-
* popup's padding both report the popup element as their target, there being
|
|
39371
|
-
* no real ::backdrop node to be one), so the rectangle is what tells them
|
|
39372
|
-
* apart. That holds as long as the box and what the popup paints are the same
|
|
39373
|
-
* thing — which stops being true for a popup with no surface of its own
|
|
39374
|
-
* (`backgroundColor="transparent"`, no shadow, no padding): what the eye reads
|
|
39375
|
-
* as backdrop is then inside the box, and a press there stays a press on the
|
|
39376
|
-
* popup.
|
|
39377
|
-
*
|
|
39378
|
-
* `data-navi-popup-outside` is how a caller says which of its own boxes are
|
|
39379
|
-
* not the surface. It is opt-in because navi cannot infer it — a background
|
|
39380
|
-
* can come from anywhere — while the caller who made the popup see-through
|
|
39381
|
-
* knows exactly which box is decoration and which is paper.
|
|
39382
|
-
*
|
|
39383
|
-
* The marker answers for the element it is on, never for its descendants: a
|
|
39384
|
-
* box painted inside a marked one is still surface, so a row can be marked and
|
|
39385
|
-
* its empty halves read as backdrop while presses on the controls it holds do
|
|
39386
|
-
* not dismiss anything. A descendant with `pointer-events: none` never becomes
|
|
39387
|
-
* a press target at all, so the marked box answers in its place — while the
|
|
39388
|
-
* same declaration on the marked box itself takes it out of hit-testing and
|
|
39389
|
-
* makes the marker unreachable (see warnAboutUnreachableOutsideRegions).
|
|
39390
|
-
*/
|
|
39391
|
-
const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
|
|
39599
|
+
return undefined;
|
|
39600
|
+
}
|
|
39601
|
+
const target = resolveExplicitTarget(source) || source;
|
|
39602
|
+
return {
|
|
39603
|
+
target,
|
|
39604
|
+
// Which way there is worn by the source, as a link wears it (see
|
|
39605
|
+
// link_replace.js): `<Button command="--navi-nav-to:/done" replace>`.
|
|
39606
|
+
implementation: () =>
|
|
39607
|
+
navTo(
|
|
39608
|
+
argument,
|
|
39609
|
+
linkAsksForReplace(source) ? { replace: true } : undefined,
|
|
39610
|
+
),
|
|
39611
|
+
};
|
|
39612
|
+
});
|
|
39392
39613
|
|
|
39393
|
-
|
|
39394
|
-
|
|
39395
|
-
|
|
39396
|
-
|
|
39397
|
-
|
|
39398
|
-
|
|
39399
|
-
|
|
39400
|
-
|
|
39401
|
-
|
|
39402
|
-
|
|
39403
|
-
|
|
39404
|
-
* own ground, and nothing in it is a dismissal.
|
|
39405
|
-
*/
|
|
39406
|
-
const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
|
|
39614
|
+
// Back to the screen the reader came from, with somewhere to land when there
|
|
39615
|
+
// is none of ours behind — a url opened cold (see navBack). The fallback is
|
|
39616
|
+
// the argument for the reason --navi-nav-to's destination is: it says WHAT the
|
|
39617
|
+
// command does when there is nothing to go back to.
|
|
39618
|
+
registerNaviCommand("--navi-nav-back", (source, event, { argument }) => {
|
|
39619
|
+
const target = resolveExplicitTarget(source) || source;
|
|
39620
|
+
return {
|
|
39621
|
+
target,
|
|
39622
|
+
implementation: () => navBack({ fallback: argument }),
|
|
39623
|
+
};
|
|
39624
|
+
});
|
|
39407
39625
|
|
|
39626
|
+
registerNaviCommand(
|
|
39627
|
+
"--navi-toggle",
|
|
39628
|
+
(source, event, { anchor, value } = {}) => {
|
|
39629
|
+
const target =
|
|
39630
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
39631
|
+
if (!target) {
|
|
39632
|
+
return undefined;
|
|
39633
|
+
}
|
|
39634
|
+
return {
|
|
39635
|
+
target,
|
|
39636
|
+
implementation: () => {
|
|
39637
|
+
const isExpanded = target.getAttribute("aria-expanded") === "true";
|
|
39638
|
+
const customEventName = isExpanded
|
|
39639
|
+
? "navi_request_close"
|
|
39640
|
+
: "navi_request_open";
|
|
39641
|
+
return dispatchCustomEvent(target, customEventName, {
|
|
39642
|
+
event,
|
|
39643
|
+
source: resolveCommandProxySource(source),
|
|
39644
|
+
anchor,
|
|
39645
|
+
// Same as --navi-open below: the half of the toggle that opens says
|
|
39646
|
+
// what it opens ON, and the half that closes carries it too so the
|
|
39647
|
+
// popup never has to ask which half it just heard.
|
|
39648
|
+
value:
|
|
39649
|
+
value === undefined ? resolveCommandValue(source, event) : value,
|
|
39650
|
+
});
|
|
39651
|
+
},
|
|
39652
|
+
};
|
|
39653
|
+
},
|
|
39654
|
+
);
|
|
39655
|
+
// A popup that edits opens ON something, and the press is the only place that
|
|
39656
|
+
// knows which one. It says it with `value` — "what this is about", as
|
|
39657
|
+
// everywhere else — rather than with an argument, which says what the command
|
|
39658
|
+
// DOES: "open" is already a complete instruction, unlike --navi-go-to-slide.
|
|
39659
|
+
// <Button value={radar.id} command="--navi-open" commandfor="radar-dialog">
|
|
39660
|
+
// The popup is told before it opens — see Dialog/Popover's `onOpen`.
|
|
39661
|
+
registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
|
|
39662
|
+
const target =
|
|
39663
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
39664
|
+
if (!target) {
|
|
39665
|
+
return undefined;
|
|
39666
|
+
}
|
|
39667
|
+
return {
|
|
39668
|
+
target,
|
|
39669
|
+
implementation: () => {
|
|
39670
|
+
// The popup reads `anchor` first and falls back to the source (see
|
|
39671
|
+
// onnavi_request_open in popover.jsx): without an `anchor`, the element
|
|
39672
|
+
// asking is the element opened against.
|
|
39673
|
+
return dispatchCustomEvent(target, "navi_request_open", {
|
|
39674
|
+
event,
|
|
39675
|
+
source: resolveCommandProxySource(source),
|
|
39676
|
+
anchor,
|
|
39677
|
+
value: value === undefined ? resolveCommandValue(source, event) : value,
|
|
39678
|
+
});
|
|
39679
|
+
},
|
|
39680
|
+
};
|
|
39681
|
+
});
|
|
39408
39682
|
/**
|
|
39409
|
-
*
|
|
39410
|
-
*
|
|
39683
|
+
* Tells the popup a source opens that a press on it has started, ahead of the
|
|
39684
|
+
* release that runs the command: what the opening will read can be asked for
|
|
39685
|
+
* now (see open_controller.js). A press rather than a hover, because what
|
|
39686
|
+
* that reads is keyed by the popup — one sheet per card — and a pointer
|
|
39687
|
+
* crossing a list of cards has chosen none of them; on a touch screen the
|
|
39688
|
+
* press is the first thing the finger says anyway.
|
|
39411
39689
|
*
|
|
39412
|
-
*
|
|
39413
|
-
*
|
|
39414
|
-
* (never an ancestor), so a press inside the popup's box never reaches it.
|
|
39690
|
+
* The target is found the way --navi-open finds it, but silently: nothing
|
|
39691
|
+
* runs here, and a source with no target says so at the release.
|
|
39415
39692
|
*/
|
|
39416
|
-
const
|
|
39417
|
-
|
|
39418
|
-
{ popupEl, openController, pressOutside },
|
|
39419
|
-
) => {
|
|
39420
|
-
if (mouseDownEvent.button !== 0) {
|
|
39693
|
+
const announceOpeningPress = (source, command) => {
|
|
39694
|
+
if (command !== "--navi-open" && command !== "--navi-toggle") {
|
|
39421
39695
|
return;
|
|
39422
39696
|
}
|
|
39423
|
-
const
|
|
39424
|
-
|
|
39697
|
+
const commandFor = source.getAttribute("commandfor");
|
|
39698
|
+
const target = commandFor
|
|
39699
|
+
? document.getElementById(commandFor)
|
|
39700
|
+
: resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
39701
|
+
if (!target) {
|
|
39425
39702
|
return;
|
|
39426
39703
|
}
|
|
39427
|
-
|
|
39428
|
-
|
|
39429
|
-
|
|
39430
|
-
|
|
39704
|
+
dispatchCustomEvent(target, "navi_open_press");
|
|
39705
|
+
};
|
|
39706
|
+
// "--navi-close:all" closes every expandable above the source, nearest first —
|
|
39707
|
+
// a link leaving from a badge shown over a sheet leaves both. A surface that
|
|
39708
|
+
// refuses (a form asking about its changes) keeps what is above it open too:
|
|
39709
|
+
// one cannot be out of the sheet while still in the badge.
|
|
39710
|
+
registerNaviCommand("--navi-close", (source, event, { argument }) => {
|
|
39711
|
+
const target =
|
|
39712
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
39713
|
+
if (!target) {
|
|
39714
|
+
return undefined;
|
|
39431
39715
|
}
|
|
39432
|
-
|
|
39433
|
-
|
|
39434
|
-
|
|
39435
|
-
|
|
39436
|
-
|
|
39437
|
-
|
|
39438
|
-
|
|
39439
|
-
|
|
39716
|
+
return {
|
|
39717
|
+
target,
|
|
39718
|
+
implementation: () => {
|
|
39719
|
+
const detail = { event, source: resolveCommandProxySource(source) };
|
|
39720
|
+
if (argument === "all") {
|
|
39721
|
+
return requestCloseUpward(target, detail);
|
|
39722
|
+
}
|
|
39723
|
+
return dispatchCustomEvent(target, "navi_request_close", detail);
|
|
39724
|
+
},
|
|
39725
|
+
};
|
|
39726
|
+
});
|
|
39727
|
+
const requestCloseUpward = (target, detail) => {
|
|
39728
|
+
let expandable = target;
|
|
39729
|
+
while (expandable) {
|
|
39730
|
+
const closing = dispatchCustomEvent(
|
|
39731
|
+
expandable,
|
|
39732
|
+
"navi_request_close",
|
|
39733
|
+
detail,
|
|
39734
|
+
);
|
|
39735
|
+
if (!closing) {
|
|
39736
|
+
return false;
|
|
39737
|
+
}
|
|
39738
|
+
const parent = expandable.parentElement;
|
|
39739
|
+
expandable = parent ? parent.closest("[aria-expanded]") : null;
|
|
39440
39740
|
}
|
|
39441
|
-
|
|
39442
|
-
|
|
39443
|
-
|
|
39741
|
+
return true;
|
|
39742
|
+
};
|
|
39743
|
+
registerNaviCommand("--navi-cancel", (source, event) => {
|
|
39744
|
+
const target =
|
|
39745
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
39746
|
+
if (!target) {
|
|
39747
|
+
return undefined;
|
|
39444
39748
|
}
|
|
39445
|
-
|
|
39446
|
-
|
|
39447
|
-
|
|
39448
|
-
|
|
39749
|
+
return {
|
|
39750
|
+
target,
|
|
39751
|
+
implementation: () => {
|
|
39752
|
+
return dispatchCustomEvent(target, "navi_request_close", {
|
|
39753
|
+
event,
|
|
39754
|
+
source: resolveCommandProxySource(source),
|
|
39755
|
+
isCancel: true,
|
|
39756
|
+
});
|
|
39757
|
+
},
|
|
39758
|
+
};
|
|
39759
|
+
});
|
|
39760
|
+
// "Yes, do it" — said inside the popup of a confirm picker (see
|
|
39761
|
+
// picker/picker_confirm.jsx), by its own yes button or by whatever a caller put
|
|
39762
|
+
// there instead. Its counterpart is --navi-cancel; the difference between the
|
|
39763
|
+
// two is this event, since both then close the popup, and a popup that closes
|
|
39764
|
+
// without having said this is the answer "no".
|
|
39765
|
+
registerNaviCommand("--navi-confirm", (source, event) => {
|
|
39766
|
+
const target =
|
|
39767
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
39768
|
+
if (!target) {
|
|
39769
|
+
return undefined;
|
|
39770
|
+
}
|
|
39771
|
+
return {
|
|
39772
|
+
target,
|
|
39773
|
+
implementation: () => {
|
|
39774
|
+
const commandSource = resolveCommandProxySource(source);
|
|
39775
|
+
dispatchCustomEvent(target, "navi_request_confirm", {
|
|
39776
|
+
event,
|
|
39777
|
+
source: commandSource,
|
|
39778
|
+
});
|
|
39779
|
+
return dispatchCustomEvent(target, "navi_request_close", {
|
|
39780
|
+
event,
|
|
39781
|
+
source: commandSource,
|
|
39782
|
+
});
|
|
39783
|
+
},
|
|
39784
|
+
};
|
|
39785
|
+
});
|
|
39786
|
+
registerNaviCommand("--navi-define", (source, event) => {
|
|
39787
|
+
const target =
|
|
39788
|
+
resolveExplicitTarget(source) || resolveClosestExpandable(source);
|
|
39789
|
+
if (!target) {
|
|
39790
|
+
return undefined;
|
|
39449
39791
|
}
|
|
39792
|
+
return {
|
|
39793
|
+
target,
|
|
39794
|
+
implementation: () => executeNaviDefine(source, event, target),
|
|
39795
|
+
};
|
|
39796
|
+
});
|
|
39797
|
+
// A send whose target is the popup itself: what is around the source IS the
|
|
39798
|
+
// popup's answer, and a picker runs its action with that answer as it closes
|
|
39799
|
+
// (see onRequestClose in picker_custom.jsx) — dispatching the action here too
|
|
39800
|
+
// would run it twice. `isSend` tells this close from a dismissal (the cross,
|
|
39801
|
+
// a press outside, Escape): the person pressed a choice, which a picker may
|
|
39802
|
+
// act on even when the choice is the one it already held.
|
|
39803
|
+
const executeNaviDefine = (source, event, target) => {
|
|
39804
|
+
return dispatchCustomEvent(target, "navi_request_close", {
|
|
39805
|
+
event,
|
|
39806
|
+
source: resolveCommandProxySource(source),
|
|
39807
|
+
isSend: true,
|
|
39808
|
+
});
|
|
39450
39809
|
};
|
|
39451
39810
|
|
|
39452
|
-
|
|
39453
|
-
|
|
39454
|
-
|
|
39455
|
-
|
|
39456
|
-
|
|
39457
|
-
* an ancestor that is still the popup's own surface. Writing them next to the
|
|
39458
|
-
* marker is the natural mistake, and it makes the marker silently unreachable
|
|
39459
|
-
* — so it is said out loud, once per open.
|
|
39460
|
-
*/
|
|
39461
|
-
const warnAboutUnreachableOutsideRegions = (popupEl) => {
|
|
39462
|
-
for (const regionEl of popupEl.querySelectorAll(
|
|
39463
|
-
`[${OUTSIDE_REGION_ATTRIBUTE}]`,
|
|
39464
|
-
)) {
|
|
39465
|
-
if (getComputedStyle(regionEl).pointerEvents === "none") {
|
|
39466
|
-
console.warn(
|
|
39467
|
-
`[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.`,
|
|
39468
|
-
regionEl,
|
|
39469
|
-
);
|
|
39470
|
-
}
|
|
39811
|
+
registerNaviCommand("--navi-scroll", (source, event) => {
|
|
39812
|
+
const target =
|
|
39813
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39814
|
+
if (!target) {
|
|
39815
|
+
return undefined;
|
|
39471
39816
|
}
|
|
39472
|
-
|
|
39817
|
+
return {
|
|
39818
|
+
target,
|
|
39819
|
+
implementation: () => {
|
|
39820
|
+
return dispatchCustomEvent(target, "navi_request_scroll", {
|
|
39821
|
+
event,
|
|
39822
|
+
id: resolveCommandValue(source, event),
|
|
39823
|
+
});
|
|
39824
|
+
},
|
|
39825
|
+
};
|
|
39826
|
+
});
|
|
39827
|
+
// The press a hand is still making, given to something that can be carried: a
|
|
39828
|
+
// hold opened a popup, and what the popup holds is to follow that same finger
|
|
39829
|
+
// without it lifting and pressing again. `event` is what the press set off (the
|
|
39830
|
+
// popup's open event), and the press is read from its chain. Run once the popup
|
|
39831
|
+
// has settled, which the caller cannot see: the carry measures its element and
|
|
39832
|
+
// puts a copy in the top layer, neither of which holds while a transition is
|
|
39833
|
+
// still moving the box (see afterPopupLift). What the carry then is — its
|
|
39834
|
+
// outcomes, its refusal — is the element's (see interaction_drag.js).
|
|
39835
|
+
registerNaviCommand("--navi-grab", (source, event) => {
|
|
39836
|
+
const target =
|
|
39837
|
+
resolveExplicitTarget(source) || source.closest("[data-drag-source]");
|
|
39838
|
+
if (!target) {
|
|
39839
|
+
return undefined;
|
|
39840
|
+
}
|
|
39841
|
+
return {
|
|
39842
|
+
target,
|
|
39843
|
+
implementation: () => {
|
|
39844
|
+
afterPopupLift(() => {
|
|
39845
|
+
// Taken away with the popup before it settled.
|
|
39846
|
+
if (!target.isConnected) {
|
|
39847
|
+
return;
|
|
39848
|
+
}
|
|
39849
|
+
dispatchCustomEvent(target, "navi_request_grab", { event });
|
|
39850
|
+
});
|
|
39851
|
+
return true;
|
|
39852
|
+
},
|
|
39853
|
+
};
|
|
39854
|
+
});
|
|
39855
|
+
registerNaviCommand("--navi-check", (source, event) => {
|
|
39856
|
+
const target =
|
|
39857
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39858
|
+
if (!target) {
|
|
39859
|
+
return undefined;
|
|
39860
|
+
}
|
|
39861
|
+
return {
|
|
39862
|
+
target,
|
|
39863
|
+
implementation: () => {
|
|
39864
|
+
return dispatchCustomEvent(target, "navi_request_check", {
|
|
39865
|
+
event,
|
|
39866
|
+
});
|
|
39867
|
+
},
|
|
39868
|
+
};
|
|
39869
|
+
});
|
|
39870
|
+
registerNaviCommand("--navi-uncheck", (source, event) => {
|
|
39871
|
+
const target =
|
|
39872
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39873
|
+
if (!target) {
|
|
39874
|
+
return undefined;
|
|
39875
|
+
}
|
|
39876
|
+
return {
|
|
39877
|
+
target,
|
|
39878
|
+
implementation: () => {
|
|
39879
|
+
return dispatchCustomEvent(target, "navi_request_uncheck", {
|
|
39880
|
+
event,
|
|
39881
|
+
});
|
|
39882
|
+
},
|
|
39883
|
+
};
|
|
39884
|
+
});
|
|
39885
|
+
registerNaviCommand("--navi-select", (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_select", {
|
|
39895
|
+
event,
|
|
39896
|
+
id: resolveCommandValue(source, event),
|
|
39897
|
+
});
|
|
39898
|
+
},
|
|
39899
|
+
};
|
|
39900
|
+
});
|
|
39901
|
+
registerNaviCommand("--navi-unselect", (source, event) => {
|
|
39902
|
+
const target =
|
|
39903
|
+
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
39904
|
+
if (!target) {
|
|
39905
|
+
return undefined;
|
|
39906
|
+
}
|
|
39907
|
+
return {
|
|
39908
|
+
target,
|
|
39909
|
+
implementation: () => {
|
|
39910
|
+
return dispatchCustomEvent(target, "navi_request_unselect", {
|
|
39911
|
+
event,
|
|
39912
|
+
id: resolveCommandValue(source, event),
|
|
39913
|
+
});
|
|
39914
|
+
},
|
|
39915
|
+
};
|
|
39916
|
+
});
|
|
39473
39917
|
|
|
39474
39918
|
/**
|
|
39475
|
-
*
|
|
39476
|
-
*
|
|
39477
|
-
*
|
|
39478
|
-
* caller asked to leave the page reachable (`backdrop={false}`).
|
|
39479
|
-
*
|
|
39480
|
-
* The press is read and never taken: no `preventDefault`, no
|
|
39481
|
-
* `stopPropagation`, and the listener is on `document` rather than on
|
|
39482
|
-
* anything covering the page — so what the press landed on hears it too, in
|
|
39483
|
-
* the same gesture, and the popup only takes note. That is the whole point of
|
|
39484
|
-
* a popup with no wall: a press outside is a press on the page, and the page
|
|
39485
|
-
* answers it. The one popup where that cannot hold is the modal dialog, where
|
|
39486
|
-
* the browser has already made everything behind inert before this runs.
|
|
39487
|
-
*
|
|
39488
|
-
* `pressEventType` says which event IS the press, and the two callers want
|
|
39489
|
-
* opposite ones:
|
|
39490
|
-
*
|
|
39491
|
-
* - `"pointerdown"` for a popup with no wall: a page with no wall over it is
|
|
39492
|
-
* a page whose own elements arbitrate their presses, and cancelling a
|
|
39493
|
-
* `pointerdown` — what a drag source and a control keeping the focus where
|
|
39494
|
-
* it is both do — suppresses every compatibility mouse event that would have
|
|
39495
|
-
* followed it. The press happens, reaches its target, and no `mousedown` is
|
|
39496
|
-
* ever dispatched for it. Reading the pointer event is reading the press
|
|
39497
|
-
* itself, whatever anything does with it afterwards, and it is the same
|
|
39498
|
-
* event the drag and surface detectors read.
|
|
39499
|
-
* - `"mousedown"` for the modal dialog, whose native `::backdrop` exists to
|
|
39500
|
-
* spend the press. The page's controls act on `mousedown`, and on a touch
|
|
39501
|
-
* screen that is not the same moment as `pointerdown`: the compatibility
|
|
39502
|
-
* mouse events are synthesized at `touchend` and hit-tested again then.
|
|
39503
|
-
* Closing on `pointerdown` takes the `::backdrop` away before that second
|
|
39504
|
-
* hit-test, so the `mousedown` lands on whatever the page holds under the
|
|
39505
|
-
* finger, and one tap both dismisses the dialog and presses the page. Read
|
|
39506
|
-
* on `mousedown`, the wall is still up when the press the page acts on is
|
|
39507
|
-
* aimed, and it catches it. Nothing behind a modal wall can cancel that
|
|
39508
|
-
* press, since nothing behind it hears the `pointerdown` at all.
|
|
39919
|
+
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
39920
|
+
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
39921
|
+
* mounted run inside the call too, exactly as they would on any other commit.
|
|
39509
39922
|
*
|
|
39510
|
-
*
|
|
39511
|
-
*
|
|
39923
|
+
* For the caller that has to read the DOM it just asked for — measuring an
|
|
39924
|
+
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
39925
|
+
* to do it, because what comes after is a browser event still in flight
|
|
39926
|
+
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
39512
39927
|
*
|
|
39513
|
-
*
|
|
39928
|
+
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
39929
|
+
* render queue drains; swapping it for "right now" for the duration of the call
|
|
39930
|
+
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
39931
|
+
* case above: rendering synchronously in the middle of an event gives up the
|
|
39932
|
+
* batching that makes several state changes one commit.
|
|
39514
39933
|
*/
|
|
39515
|
-
const
|
|
39516
|
-
|
|
39517
|
-
|
|
39518
|
-
)
|
|
39519
|
-
const onDocumentPointerDown = (pointerDownEvent) => {
|
|
39520
|
-
if (pointerDownEvent.button !== 0) {
|
|
39521
|
-
return;
|
|
39522
|
-
}
|
|
39523
|
-
if (openedDuringThisPress(openController)) {
|
|
39524
|
-
// The release of the press that opened it is not somebody dismissing it
|
|
39525
|
-
// (see openedDuringThisPress).
|
|
39526
|
-
return;
|
|
39527
|
-
}
|
|
39528
|
-
// The press landed inside another popup: that is a press on what is in
|
|
39529
|
-
// front, not outside. Asking the target where it lives rather than asking
|
|
39530
|
-
// this popup whether it was pushed — a popup in front does not have to be
|
|
39531
|
-
// one this popup knows about. In front means neither inside nor around: a
|
|
39532
|
-
// popup nested in this one falls to the containment check below as the
|
|
39533
|
-
// inside press it is, and one this popup opened INSIDE (a bubble rendered
|
|
39534
|
-
// in a sheet's content) is around it, so what that popup holds is outside
|
|
39535
|
-
// this one exactly like the rest of the page.
|
|
39536
|
-
const popupUnderPointer = pointerDownEvent.target.closest?.(
|
|
39537
|
-
`[navi-control="dialog"], [navi-control="popover"]`,
|
|
39538
|
-
);
|
|
39539
|
-
if (
|
|
39540
|
-
popupUnderPointer &&
|
|
39541
|
-
popupUnderPointer !== popupEl &&
|
|
39542
|
-
!popupEl.contains(popupUnderPointer) &&
|
|
39543
|
-
!popupUnderPointer.contains(popupEl)
|
|
39544
|
-
) {
|
|
39545
|
-
return;
|
|
39546
|
-
}
|
|
39547
|
-
if (
|
|
39548
|
-
popupEl.id &&
|
|
39549
|
-
pointerDownEvent.target.closest?.(
|
|
39550
|
-
`[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupEl.id)}"]`,
|
|
39551
|
-
)
|
|
39552
|
-
) {
|
|
39553
|
-
return;
|
|
39554
|
-
}
|
|
39555
|
-
// Real DOM containment wins over the rectangle below — an element
|
|
39556
|
-
// genuinely inside the popup (`overflow: visible`, a negative margin, an
|
|
39557
|
-
// absolutely-positioned child) can be painted outside its border box, and
|
|
39558
|
-
// a press there is not outside just because its coordinates are. The
|
|
39559
|
-
// popup element itself is excluded (contains() answers true for it): a
|
|
39560
|
-
// press on a modal dialog's native backdrop reports the dialog as its
|
|
39561
|
-
// target, there being no `::backdrop` node to be one, so treating that as
|
|
39562
|
-
// contained would keep the rectangle from ever running.
|
|
39563
|
-
if (
|
|
39564
|
-
pointerDownEvent.target !== popupEl &&
|
|
39565
|
-
popupEl.contains(pointerDownEvent.target)
|
|
39566
|
-
) {
|
|
39567
|
-
return;
|
|
39568
|
-
}
|
|
39569
|
-
const rect = popupEl.getBoundingClientRect();
|
|
39570
|
-
const isOutside =
|
|
39571
|
-
pointerDownEvent.clientX < rect.left ||
|
|
39572
|
-
pointerDownEvent.clientX > rect.right ||
|
|
39573
|
-
pointerDownEvent.clientY < rect.top ||
|
|
39574
|
-
pointerDownEvent.clientY > rect.bottom;
|
|
39575
|
-
if (!isOutside) {
|
|
39576
|
-
return;
|
|
39577
|
-
}
|
|
39578
|
-
openController.requestClose(pointerDownEvent, {
|
|
39579
|
-
isCancel: pressOutside === "cancel",
|
|
39580
|
-
});
|
|
39581
|
-
};
|
|
39582
|
-
document.addEventListener(pressEventType, onDocumentPointerDown, {
|
|
39583
|
-
capture: true,
|
|
39584
|
-
});
|
|
39585
|
-
return () => {
|
|
39586
|
-
document.removeEventListener(pressEventType, onDocumentPointerDown, {
|
|
39587
|
-
capture: true,
|
|
39588
|
-
});
|
|
39934
|
+
const flushSyncRendering = (fn) => {
|
|
39935
|
+
const debounceRenderingPrevious = options.debounceRendering;
|
|
39936
|
+
options.debounceRendering = (drainRenderQueue) => {
|
|
39937
|
+
drainRenderQueue();
|
|
39589
39938
|
};
|
|
39939
|
+
try {
|
|
39940
|
+
fn();
|
|
39941
|
+
} finally {
|
|
39942
|
+
options.debounceRendering = debounceRenderingPrevious;
|
|
39943
|
+
}
|
|
39590
39944
|
};
|
|
39591
39945
|
|
|
39592
39946
|
/**
|
|
@@ -66899,6 +67253,13 @@ const css$D = /* css */`
|
|
|
66899
67253
|
* is a `Popover`-level answer only for `"close"`/`"cancel"`; `"capture"` has
|
|
66900
67254
|
* nothing left to absorb without a wall, and `"ignore"` renders no wall anyway.
|
|
66901
67255
|
* See docs/popup_backdrop.md.
|
|
67256
|
+
* @param {string} [props.group] - The popover's peers: the triggers carrying
|
|
67257
|
+
* `data-navi-popup-group` with this name. A press on one, while the popover
|
|
67258
|
+
* is open, closes it and opens the peer's popup in that same press — a
|
|
67259
|
+
* menu bar, where pressing another menu switches to it. The wall still
|
|
67260
|
+
* takes the press (the page behind stays out of reach, hover included) and
|
|
67261
|
+
* hands its click to the peer it covered. Without a wall there is nothing
|
|
67262
|
+
* to ask: the press reaches the peer on its own.
|
|
66902
67263
|
* @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="ignore"]
|
|
66903
67264
|
* - `"ignore"` (default): no backdrop at all, outside presses pass straight
|
|
66904
67265
|
* through. `"close"` closes the popover on an outside press, `"cancel"`
|
|
@@ -67275,6 +67636,7 @@ const usePopoverProps = props => {
|
|
|
67275
67636
|
// which is a sibling element (see this file's top comment).
|
|
67276
67637
|
backdropColor,
|
|
67277
67638
|
backdropFilter,
|
|
67639
|
+
group,
|
|
67278
67640
|
scrollCapture,
|
|
67279
67641
|
focusCapture,
|
|
67280
67642
|
// "auto" (default) → the popover follows its content. "frozen" → measured
|
|
@@ -67927,13 +68289,21 @@ const usePopoverProps = props => {
|
|
|
67927
68289
|
}
|
|
67928
68290
|
if (backdropEl) {
|
|
67929
68291
|
backdropEl.setAttribute("aria-expanded", "false");
|
|
67930
|
-
disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent,
|
|
68292
|
+
disarmBackdropHideRef.current = armPointerDownOutsideClose(closeEvent, clickEvent => {
|
|
67931
68293
|
// Set regardless of isTopLayer — see openEffect's own identical
|
|
67932
68294
|
// comment for why hidePopover() alone isn't reliably sufficient.
|
|
67933
68295
|
backdropEl.setAttribute("navi-hidden", "");
|
|
67934
68296
|
if (isTopLayer) {
|
|
67935
68297
|
backdropEl.hidePopover();
|
|
67936
68298
|
}
|
|
68299
|
+
if (group && clickEvent) {
|
|
68300
|
+
handWallClickToPeer(clickEvent, {
|
|
68301
|
+
wallEl: backdropEl,
|
|
68302
|
+
popupEl: popoverEl,
|
|
68303
|
+
anchorEl: anchorElement,
|
|
68304
|
+
group
|
|
68305
|
+
});
|
|
68306
|
+
}
|
|
67937
68307
|
});
|
|
67938
68308
|
}
|
|
67939
68309
|
restoreFocus?.(closeEvent);
|
|
@@ -68355,6 +68725,10 @@ const Popup = props => {
|
|
|
68355
68725
|
scrollCapture,
|
|
68356
68726
|
positionAreaFixed,
|
|
68357
68727
|
positionAreaWhenAnchorIsInvalid,
|
|
68728
|
+
// A top-layer dialog's wall is its native ::backdrop, which goes with the
|
|
68729
|
+
// dialog on the press that closes it: no click is left on it to hand to a
|
|
68730
|
+
// peer (see handWallClickToPeer).
|
|
68731
|
+
group,
|
|
68358
68732
|
// Dialog-only, destructured out for the same reason: forwarded below only
|
|
68359
68733
|
// in the dialog branch, so they never reach the popover element.
|
|
68360
68734
|
dockedOnSmallTouchScreen,
|
|
@@ -68404,6 +68778,7 @@ const Popup = props => {
|
|
|
68404
68778
|
positionAreaWhenAnchorIsInvalid: positionAreaWhenAnchorIsInvalid,
|
|
68405
68779
|
scrollCapture: scrollCapture === "popover" || scrollCapture,
|
|
68406
68780
|
positionAreaFixed: positionAreaFixed,
|
|
68781
|
+
group: group,
|
|
68407
68782
|
className: withPropsClassName("navi_popup", className),
|
|
68408
68783
|
expand: expand,
|
|
68409
68784
|
expandX: expandX,
|
|
@@ -69321,13 +69696,18 @@ const PickerContentInsidePopup = props => {
|
|
|
69321
69696
|
calloutIcon,
|
|
69322
69697
|
calloutCloseButton,
|
|
69323
69698
|
calloutCloseByScroll,
|
|
69699
|
+
popupGroup,
|
|
69324
69700
|
...rest
|
|
69325
69701
|
} = props;
|
|
69326
69702
|
const isPopover = mode === "popover";
|
|
69327
69703
|
const isCallout = mode === "callout";
|
|
69328
69704
|
return jsx(Next, {
|
|
69329
69705
|
"aria-haspopup": isPopover ? "listbox" : "dialog",
|
|
69330
|
-
"navi-popover-mode": isPopover ? popoverMode : undefined
|
|
69706
|
+
"navi-popover-mode": isPopover ? popoverMode : undefined
|
|
69707
|
+
// On the trigger, whatever the mode: a peer's popup reads it off the
|
|
69708
|
+
// press that lands here (see `group` in callout.js).
|
|
69709
|
+
,
|
|
69710
|
+
"data-navi-popup-group": popupGroup,
|
|
69331
69711
|
...rest,
|
|
69332
69712
|
// On popupProps already (see the picker's popup assembly); they mean
|
|
69333
69713
|
// nothing to the picker element.
|
|
@@ -69363,7 +69743,12 @@ const PickerContentInsidePopup = props => {
|
|
|
69363
69743
|
status: calloutStatus,
|
|
69364
69744
|
icon: calloutIcon,
|
|
69365
69745
|
closeButton: calloutCloseButton,
|
|
69366
|
-
closeByScroll: calloutCloseByScroll
|
|
69746
|
+
closeByScroll: calloutCloseByScroll
|
|
69747
|
+
// Left unsaid, the callout's own default: no backdrop. It dims
|
|
69748
|
+
// nothing, so the page looks as pressable as it did before.
|
|
69749
|
+
,
|
|
69750
|
+
backdrop: backdrop,
|
|
69751
|
+
group: popupGroup,
|
|
69367
69752
|
children: jsx(PopupModeContext.Provider, {
|
|
69368
69753
|
value: mode,
|
|
69369
69754
|
children: children
|
|
@@ -69400,6 +69785,7 @@ const PickerContentInsidePopup = props => {
|
|
|
69400
69785
|
backdropVariant: backdropVariant,
|
|
69401
69786
|
backdropColor: backdropColor,
|
|
69402
69787
|
backdropFilter: backdropFilter,
|
|
69788
|
+
group: popupGroup,
|
|
69403
69789
|
focusCapture: isPopover ? focusCapture : undefined,
|
|
69404
69790
|
expand: isPopover ? undefined : dialogExpand,
|
|
69405
69791
|
expandX: isPopover ? undefined : dialogExpandX,
|
|
@@ -69459,6 +69845,8 @@ const PickerCalloutPopup = ({
|
|
|
69459
69845
|
icon,
|
|
69460
69846
|
closeButton,
|
|
69461
69847
|
closeByScroll,
|
|
69848
|
+
backdrop,
|
|
69849
|
+
group,
|
|
69462
69850
|
onnavi_request_open,
|
|
69463
69851
|
onnavi_request_close,
|
|
69464
69852
|
onnavi_request_confirm,
|
|
@@ -69500,6 +69888,8 @@ const PickerCalloutPopup = ({
|
|
|
69500
69888
|
icon,
|
|
69501
69889
|
closeButton,
|
|
69502
69890
|
closeByScroll,
|
|
69891
|
+
backdrop,
|
|
69892
|
+
group,
|
|
69503
69893
|
anchorElement,
|
|
69504
69894
|
// The request, chained to the press that made it: the callout reads the
|
|
69505
69895
|
// mousedown off it to wait for the release before listening for a click
|
|
@@ -71666,7 +72056,13 @@ const ListRowContext = createContext(null);
|
|
|
71666
72056
|
// row takes its place in the collection by slot: the place is then the list's
|
|
71667
72057
|
// to move, and the row's to follow — see list_rows.js.
|
|
71668
72058
|
const ListSlotContext = createContext(null);
|
|
71669
|
-
const css$x = /* css */`@
|
|
72059
|
+
const css$x = /* css */`@property --list-group-label-height {
|
|
72060
|
+
syntax: "<length>";
|
|
72061
|
+
inherits: false;
|
|
72062
|
+
initial-value: 0;
|
|
72063
|
+
}
|
|
72064
|
+
|
|
72065
|
+
@layer navi {
|
|
71670
72066
|
.navi_list_container {
|
|
71671
72067
|
--list-outline-width: 1px;
|
|
71672
72068
|
--list-border-radius: 4px;
|
|
@@ -72065,6 +72461,7 @@ const css$x = /* css */`@layer navi {
|
|
|
72065
72461
|
}
|
|
72066
72462
|
|
|
72067
72463
|
& .navi_list_item_group_list {
|
|
72464
|
+
--list-group-label-height: inherit;
|
|
72068
72465
|
flex-direction: column;
|
|
72069
72466
|
width: 100%;
|
|
72070
72467
|
margin: 0;
|
|
@@ -72072,8 +72469,12 @@ const css$x = /* css */`@layer navi {
|
|
|
72072
72469
|
list-style: none;
|
|
72073
72470
|
display: flex;
|
|
72074
72471
|
|
|
72472
|
+
& > .navi_list_item {
|
|
72473
|
+
--list-group-label-height: inherit;
|
|
72474
|
+
}
|
|
72475
|
+
|
|
72075
72476
|
& .navi_list_item {
|
|
72076
|
-
scroll-margin-top: calc(var(--x-list-scroll-spacing-top) + var(--list-group-label-height
|
|
72477
|
+
scroll-margin-top: calc(var(--x-list-scroll-spacing-top) + var(--list-group-label-height));
|
|
72077
72478
|
}
|
|
72078
72479
|
}
|
|
72079
72480
|
|
|
@@ -79164,6 +79565,7 @@ const PickerFirstResolver = props => {
|
|
|
79164
79565
|
* escapeEffect?: "cancel" | "close",
|
|
79165
79566
|
* pressOutside?: "close" | "cancel" | "capture",
|
|
79166
79567
|
* backdrop?: boolean,
|
|
79568
|
+
* popupGroup?: string,
|
|
79167
79569
|
* backdropVariant?: "auto" | "lift" | "discrete" | "invisible",
|
|
79168
79570
|
* backdropColor?: string,
|
|
79169
79571
|
* backdropFilter?: string,
|
|
@@ -79563,10 +79965,23 @@ const PickerFirstResolver = props => {
|
|
|
79563
79965
|
* and the page at all. `false` lets a press outside both close the popup and
|
|
79564
79966
|
* reach whatever it landed on, in one gesture — for a picker opened over a
|
|
79565
79967
|
* page that stays as pressable as it looks (a plan, a map, a canvas), where a
|
|
79566
|
-
* wall would spend the first press on dismissing. Honoured in
|
|
79968
|
+
* wall would spend the first press on dismissing. Honoured in every mode: a
|
|
79567
79969
|
* dialog with no wall is shown through the Popover API rather than
|
|
79568
79970
|
* `showModal()`, so a picker docked as a bottom sheet on a phone still
|
|
79569
|
-
* leaves the plan behind it live.
|
|
79971
|
+
* leaves the plan behind it live. `false` by default under `mode="callout"`,
|
|
79972
|
+
* a bubble that dims nothing; `true` there spends the press outside on
|
|
79973
|
+
* closing it — the link under it not followed, the card under it not held —
|
|
79974
|
+
* without a wall: the scroll a finger starts beside the bubble still
|
|
79975
|
+
* scrolls, and a peer (`popupGroup`) still gets its press.
|
|
79976
|
+
* @param {string} [popupGroup] Pickers sharing the name are peers, whatever
|
|
79977
|
+
* their mode: pressing one while another's popup is open goes from that
|
|
79978
|
+
* popup to this one in a single press — a menu bar, where pressing another
|
|
79979
|
+
* menu switches to it. A popover's wall still takes that press — the page
|
|
79980
|
+
* behind stays out of reach, hover included — and hands its click to the
|
|
79981
|
+
* peer it covered; a callout with `backdrop` closes on it without spending
|
|
79982
|
+
* it. Not read in dialog mode: a dialog's wall goes with it on the press
|
|
79983
|
+
* that closes it. Written on the trigger as `data-navi-popup-group`, which
|
|
79984
|
+
* is also how anything else joins the group.
|
|
79570
79985
|
* @param {"auto"|"lift"|"discrete"|"invisible"} [backdropVariant="auto"] How
|
|
79571
79986
|
* visible the popup's backdrop is, independently of what a click outside
|
|
79572
79987
|
* does: `"auto"` is the paint `pressOutside` implies,
|