@jsenv/navi 0.29.174 → 0.29.175
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/jsenv_navi.js +232 -38
- package/dist/jsenv_navi.js.map +15 -15
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
6
6
|
export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
|
|
7
|
-
import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, scrollIntoViewScoped, initFocusGroup, isTouchDrivenEvent, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
7
|
+
import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, scrollIntoViewScoped, initFocusGroup, isTouchDrivenEvent, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
8
8
|
export { chainEvent, clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
9
9
|
import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
|
|
10
10
|
import { isValidElement, h, Fragment, createContext, render, toChildArray, options, cloneElement } from "preact";
|
|
@@ -2905,6 +2905,15 @@ const compareTwoJsValues = (
|
|
|
2905
2905
|
if (aKeys.length !== bKeys.length) {
|
|
2906
2906
|
return false;
|
|
2907
2907
|
}
|
|
2908
|
+
// The same number of keys is not the same keys, and only a's are walked
|
|
2909
|
+
// below. A key b does not have reads as undefined, so { include: undefined }
|
|
2910
|
+
// and { search: "a" } would compare "include" (undefined on both sides),
|
|
2911
|
+
// never look at "search", and pass for equal.
|
|
2912
|
+
for (const bKey of bKeys) {
|
|
2913
|
+
if (!Object.hasOwn(a, bKey)) {
|
|
2914
|
+
return false;
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2908
2917
|
if (aKeys.length === 0 && (!isPlainObject$1(a) || !isPlainObject$1(b))) {
|
|
2909
2918
|
// A Set, a Map, an element, a URL: nothing enumerable to tell two of them
|
|
2910
2919
|
// apart, so they are the same one or they are not — and they are not,
|
|
@@ -11529,6 +11538,17 @@ defineInteractionDetector({
|
|
|
11529
11538
|
undo.push(() => {
|
|
11530
11539
|
element.removeEventListener("dragstart", onDragStart);
|
|
11531
11540
|
});
|
|
11541
|
+
|
|
11542
|
+
// A touch this element may take has to be refusable before the finger
|
|
11543
|
+
// lands, or the browser can cancel an established swipe mid-gesture by
|
|
11544
|
+
// scrolling the list around it along the axis touch-action leaves free —
|
|
11545
|
+
// same rule, same moment as the attributes above (see keepTouchRefusable).
|
|
11546
|
+
element.addEventListener("touchmove", keepTouchRefusable, {
|
|
11547
|
+
passive: false
|
|
11548
|
+
});
|
|
11549
|
+
undo.push(() => {
|
|
11550
|
+
element.removeEventListener("touchmove", keepTouchRefusable);
|
|
11551
|
+
});
|
|
11532
11552
|
}
|
|
11533
11553
|
if (hasLongPress) {
|
|
11534
11554
|
mark(LONGPRESS_ATTRIBUTE, "");
|
|
@@ -31190,7 +31210,14 @@ const RouteTravel = ({
|
|
|
31190
31210
|
,
|
|
31191
31211
|
|
|
31192
31212
|
"data-travel-by-drag": travelByDrag ? axis : undefined,
|
|
31193
|
-
"data-travel-by-wheel": travelByDrag ? axis : undefined
|
|
31213
|
+
"data-travel-by-wheel": travelByDrag ? axis : undefined
|
|
31214
|
+
// A touch this box may take has to be refusable before the finger lands,
|
|
31215
|
+
// or the browser can cancel an established travel mid-swipe by scrolling
|
|
31216
|
+
// the axis touch-action leaves it — see keepTouchRefusable for why a JSX
|
|
31217
|
+
// prop is enough (an element-level touchmove listener is non-passive).
|
|
31218
|
+
,
|
|
31219
|
+
|
|
31220
|
+
onTouchMove: travelByDrag ? keepTouchRefusable : undefined,
|
|
31194
31221
|
onPointerDown: onPointerDown,
|
|
31195
31222
|
children: children
|
|
31196
31223
|
});
|
|
@@ -34130,6 +34157,11 @@ const useUIStateController = (
|
|
|
34130
34157
|
|
|
34131
34158
|
state: stateInitial,
|
|
34132
34159
|
uiState: stateInitial,
|
|
34160
|
+
// What a bound signal last said — the change detector for writes coming
|
|
34161
|
+
// from the outside. Kept apart from `state` on purpose: `state` is the
|
|
34162
|
+
// last ACCEPTED value (the rollback target), and a signal echoing the
|
|
34163
|
+
// control's own unconfirmed write moves this field without moving it.
|
|
34164
|
+
stateFromSignalPrevious: stateInitial,
|
|
34133
34165
|
uiStateSignal,
|
|
34134
34166
|
// What this control holds by ITSELF — the same signal as above unless
|
|
34135
34167
|
// it is a button inheriting from the control around it (see `inherit`).
|
|
@@ -34640,22 +34672,43 @@ const useUIStateController = (
|
|
|
34640
34672
|
if (controller.hasStateProp) {
|
|
34641
34673
|
controller.hasStateProp = false;
|
|
34642
34674
|
controller.state = stateInitial;
|
|
34675
|
+
controller.stateFromSignalPrevious = stateInitial;
|
|
34643
34676
|
}
|
|
34644
34677
|
if (controlInfo.signal) {
|
|
34645
34678
|
// The other half of a bound signal: the control follows it when
|
|
34646
|
-
// something else writes it. Compared against
|
|
34647
|
-
//
|
|
34679
|
+
// something else writes it. Compared against what the signal last
|
|
34680
|
+
// said, never against the ui state — otherwise typing into an
|
|
34648
34681
|
// uncontrolled control would be undone on the next render. A write
|
|
34649
34682
|
// coming from the control itself lands here with the value already in
|
|
34650
34683
|
// place, and setUIState treats an unchanged state as a no-op.
|
|
34651
|
-
|
|
34652
|
-
|
|
34653
|
-
|
|
34654
|
-
|
|
34655
|
-
|
|
34656
|
-
|
|
34657
|
-
|
|
34658
|
-
|
|
34684
|
+
if (
|
|
34685
|
+
!compareTwoJsValues(
|
|
34686
|
+
stateFromSignal,
|
|
34687
|
+
controller.stateFromSignalPrevious,
|
|
34688
|
+
)
|
|
34689
|
+
) {
|
|
34690
|
+
controller.stateFromSignalPrevious = stateFromSignal;
|
|
34691
|
+
// On a control that runs an action, a signal matching the ui state
|
|
34692
|
+
// is the echo of the control's own write (setUIState wrote the
|
|
34693
|
+
// signal at the press — for a picker, at the pick, before the
|
|
34694
|
+
// close even runs the action). `state` is the rollback target —
|
|
34695
|
+
// the last value the outside accepted — and the echo must not
|
|
34696
|
+
// become it: resetOnError would then "restore" the very value the
|
|
34697
|
+
// server is about to refuse. The action settles `state` on its
|
|
34698
|
+
// own: success acknowledges (state = uiState), failure resets to
|
|
34699
|
+
// the state kept here. A control with no action has no such
|
|
34700
|
+
// settlement, so it keeps following the signal.
|
|
34701
|
+
const signalEchoesOwnWrite =
|
|
34702
|
+
Boolean(props.action) &&
|
|
34703
|
+
compareTwoJsValues(stateFromSignal, controller.uiState);
|
|
34704
|
+
if (!signalEchoesOwnWrite) {
|
|
34705
|
+
controller.state = stateFromSignal;
|
|
34706
|
+
if (!optimisticWorkInFlight) {
|
|
34707
|
+
controller.setUIState(
|
|
34708
|
+
stateFromSignal,
|
|
34709
|
+
new CustomEvent("state_prop_change"),
|
|
34710
|
+
);
|
|
34711
|
+
}
|
|
34659
34712
|
}
|
|
34660
34713
|
}
|
|
34661
34714
|
}
|
|
@@ -37881,6 +37934,30 @@ const useInteractiveProps = (props, {
|
|
|
37881
37934
|
resetOnError,
|
|
37882
37935
|
optimistic
|
|
37883
37936
|
} = props;
|
|
37937
|
+
// The DOM's word for the unconfirmed moment: an optimistic control paints
|
|
37938
|
+
// the value before the server accepts it, and stays interactive while the
|
|
37939
|
+
// run is out — aria-busy says "false" on purpose. This attribute is the
|
|
37940
|
+
// only trace of that moment, for an app that wants to dim or mark the
|
|
37941
|
+
// value until it is confirmed: `[data-optimistic]` on the control host and
|
|
37942
|
+
// its control root. Written at the run's boundaries rather than from
|
|
37943
|
+
// render: the bound action is a proxy following the UI state, so once the
|
|
37944
|
+
// state moves ahead of the running instance the proxy stops reporting the
|
|
37945
|
+
// run (same trap as the queue below).
|
|
37946
|
+
const syncOptimisticAttribute = on => {
|
|
37947
|
+
const el = ref.current;
|
|
37948
|
+
if (!el) {
|
|
37949
|
+
return;
|
|
37950
|
+
}
|
|
37951
|
+
const controlRoot = findControlRoot(el);
|
|
37952
|
+
const targets = controlRoot && controlRoot !== el ? [el, controlRoot] : [el];
|
|
37953
|
+
for (const target of targets) {
|
|
37954
|
+
if (on) {
|
|
37955
|
+
target.setAttribute("data-optimistic", "");
|
|
37956
|
+
} else {
|
|
37957
|
+
target.removeAttribute("data-optimistic");
|
|
37958
|
+
}
|
|
37959
|
+
}
|
|
37960
|
+
};
|
|
37884
37961
|
Object.assign(controlHostProps, {
|
|
37885
37962
|
onFocus: e => {
|
|
37886
37963
|
// Transfer programmatic focus to the delegate target (navi-focus-delegate or navi-control-proxy-for)
|
|
@@ -37967,6 +38044,9 @@ const useInteractiveProps = (props, {
|
|
|
37967
38044
|
uiStateController.pendingActionEvent = e.detail.event;
|
|
37968
38045
|
uiStateController.actionInFlight = true;
|
|
37969
38046
|
uiStateController.parallelGuard?.claim(uiStateController);
|
|
38047
|
+
if (optimistic) {
|
|
38048
|
+
syncOptimisticAttribute(true);
|
|
38049
|
+
}
|
|
37970
38050
|
// The very instance this run uses, resolved now — while the UI state
|
|
37971
38051
|
// still holds the value the run was made for. detail.action may be a
|
|
37972
38052
|
// proxy following that state, and by the time anyone wants to abort
|
|
@@ -37984,6 +38064,7 @@ const useInteractiveProps = (props, {
|
|
|
37984
38064
|
uiStateController.parallelGuard?.release(uiStateController);
|
|
37985
38065
|
const queuedEvent = uiStateController.queuedActionAllowedEvent;
|
|
37986
38066
|
if (!queuedEvent) {
|
|
38067
|
+
syncOptimisticAttribute(false);
|
|
37987
38068
|
return;
|
|
37988
38069
|
}
|
|
37989
38070
|
if (outcome.error) {
|
|
@@ -37991,6 +38072,7 @@ const useInteractiveProps = (props, {
|
|
|
37991
38072
|
// known state (resetOnError above), and what was queued was built
|
|
37992
38073
|
// on top of the state that just failed.
|
|
37993
38074
|
uiStateController.queuedActionAllowedEvent = null;
|
|
38075
|
+
syncOptimisticAttribute(false);
|
|
37994
38076
|
return;
|
|
37995
38077
|
}
|
|
37996
38078
|
// A microtask later, not right here: this runs inside the batch()
|
|
@@ -45869,32 +45951,56 @@ const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
|
|
|
45869
45951
|
* use_displayed_layout_effect.js).
|
|
45870
45952
|
*
|
|
45871
45953
|
* The `mount` prop moves that line. "closed" is two states, not one — never
|
|
45872
|
-
* opened yet, and closed again after an opening — and the
|
|
45954
|
+
* opened yet, and closed again after an opening — and the four values answer
|
|
45873
45955
|
* both at once:
|
|
45874
45956
|
*
|
|
45875
|
-
* | mount | before the first open
|
|
45876
|
-
* | ----------------- |
|
|
45877
|
-
* | "always" | mounted
|
|
45878
|
-
* | "
|
|
45879
|
-
* | "
|
|
45957
|
+
* | mount | before the first open | after a close |
|
|
45958
|
+
* | ----------------- | --------------------------- | ------------- |
|
|
45959
|
+
* | "always" | mounted | mounted |
|
|
45960
|
+
* | "idle" | mounted once the page idles | mounted |
|
|
45961
|
+
* | "from-first-open" | not mounted | mounted |
|
|
45962
|
+
* | "while-opened" | not mounted | not mounted |
|
|
45880
45963
|
*
|
|
45881
45964
|
* "always" is for content something else depends on before any opening: a
|
|
45882
45965
|
* value the popup's owner reads off its own children, fields a form around it
|
|
45883
45966
|
* collects on submit, a size measured from outside.
|
|
45884
45967
|
*
|
|
45968
|
+
* "idle" is "always" minus the cost on the critical render: the page appears
|
|
45969
|
+
* without the content, and the browser builds it in an idle moment after
|
|
45970
|
+
* load — so by the time anyone clicks, it is usually already there.
|
|
45971
|
+
*
|
|
45885
45972
|
* "while-opened" is the opposite end: content that must be rebuilt from
|
|
45886
45973
|
* scratch every time, because what it shows is read once at build time and can
|
|
45887
45974
|
* change while the popup is closed — an uncontrolled field seeded from a
|
|
45888
45975
|
* `defaultValue`, a form whose fresh state is its initial state.
|
|
45976
|
+
*
|
|
45977
|
+
* On top of whichever value is picked, intent on the anchor warms the content:
|
|
45978
|
+
* a pointer entering the popup's anchor, or focus landing in it, builds the
|
|
45979
|
+
* content ahead of the click that will open it. Deferring the build to the
|
|
45980
|
+
* opening puts its whole cost in the frame right after the click — the frame
|
|
45981
|
+
* where a delay is felt hardest — while the ~100-300ms between hovering a
|
|
45982
|
+
* trigger and pressing it are free. The warming render is asynchronous
|
|
45983
|
+
* (batched, not flushed): nothing here needs the content in the DOM before
|
|
45984
|
+
* the click, only before the open that follows it.
|
|
45889
45985
|
*/
|
|
45890
45986
|
|
|
45891
45987
|
|
|
45892
45988
|
const MOUNT_DEFAULT = "from-first-open";
|
|
45893
45989
|
|
|
45990
|
+
// requestIdleCallback is missing from Safari; a timeout is close enough there.
|
|
45991
|
+
const requestIdle = (callback) =>
|
|
45992
|
+
typeof requestIdleCallback === "function"
|
|
45993
|
+
? requestIdleCallback(callback)
|
|
45994
|
+
: setTimeout(callback, 300);
|
|
45995
|
+
const cancelIdle = (id) =>
|
|
45996
|
+
typeof cancelIdleCallback === "function"
|
|
45997
|
+
? cancelIdleCallback(id)
|
|
45998
|
+
: clearTimeout(id);
|
|
45999
|
+
|
|
45894
46000
|
const usePopupContentMount = (
|
|
45895
46001
|
openController,
|
|
45896
46002
|
ref,
|
|
45897
|
-
{ mount = MOUNT_DEFAULT },
|
|
46003
|
+
{ mount = MOUNT_DEFAULT, anchor },
|
|
45898
46004
|
) => {
|
|
45899
46005
|
const mountedAlways = mount === "always";
|
|
45900
46006
|
const [contentMounted, setContentMounted] = useState(
|
|
@@ -45933,6 +46039,42 @@ const usePopupContentMount = (
|
|
|
45933
46039
|
setContentMounted(true);
|
|
45934
46040
|
}
|
|
45935
46041
|
}, [mountedAlways]);
|
|
46042
|
+
useEffect(() => {
|
|
46043
|
+
if (mount !== "idle" || contentMounted) {
|
|
46044
|
+
return undefined;
|
|
46045
|
+
}
|
|
46046
|
+
const idleId = requestIdle(() => {
|
|
46047
|
+
setContentMounted(true);
|
|
46048
|
+
});
|
|
46049
|
+
return () => {
|
|
46050
|
+
cancelIdle(idleId);
|
|
46051
|
+
};
|
|
46052
|
+
}, [mount, contentMounted]);
|
|
46053
|
+
// Warm on intent (see the top comment). The anchor accepts the same shapes
|
|
46054
|
+
// Popover resolves at open time — a string id, a ref, an element — but is
|
|
46055
|
+
// resolved here at effect time: an id that matches nothing yet simply
|
|
46056
|
+
// doesn't warm, the open still mounts the content like it always does.
|
|
46057
|
+
useEffect(() => {
|
|
46058
|
+
if (contentMounted || !anchor) {
|
|
46059
|
+
return undefined;
|
|
46060
|
+
}
|
|
46061
|
+
const anchorElement =
|
|
46062
|
+
typeof anchor === "string"
|
|
46063
|
+
? document.getElementById(anchor)
|
|
46064
|
+
: (anchor.current ?? anchor);
|
|
46065
|
+
if (!anchorElement) {
|
|
46066
|
+
return undefined;
|
|
46067
|
+
}
|
|
46068
|
+
const warm = () => {
|
|
46069
|
+
setContentMounted(true);
|
|
46070
|
+
};
|
|
46071
|
+
anchorElement.addEventListener("pointerenter", warm);
|
|
46072
|
+
anchorElement.addEventListener("focusin", warm);
|
|
46073
|
+
return () => {
|
|
46074
|
+
anchorElement.removeEventListener("pointerenter", warm);
|
|
46075
|
+
anchorElement.removeEventListener("focusin", warm);
|
|
46076
|
+
};
|
|
46077
|
+
}, [contentMounted, anchor]);
|
|
45936
46078
|
|
|
45937
46079
|
return contentMounted;
|
|
45938
46080
|
};
|
|
@@ -46167,7 +46309,7 @@ const useExpandableContext = partName => {
|
|
|
46167
46309
|
* openDirection?: "down" | "up" | "right" | "left",
|
|
46168
46310
|
* autoFocus?: boolean,
|
|
46169
46311
|
* maxContentHeight?: string | number,
|
|
46170
|
-
* mount?: "always" | "from-first-open" | "while-opened",
|
|
46312
|
+
* mount?: "always" | "idle" | "from-first-open" | "while-opened",
|
|
46171
46313
|
* arrowKeyShortcuts?: boolean,
|
|
46172
46314
|
* openKeyShortcut?: string,
|
|
46173
46315
|
* closeKeyShortcut?: string,
|
|
@@ -46222,14 +46364,17 @@ const useExpandableContext = partName => {
|
|
|
46222
46364
|
* @param maxContentHeight - Caps the content height; taller content scrolls
|
|
46223
46365
|
* inside the expandable instead of growing it.
|
|
46224
46366
|
* @param mount - When the content is built and thrown away, a popup's own
|
|
46225
|
-
*
|
|
46367
|
+
* values and a popup's own code (see popup_content_mount.js). `"from-first-open"` (the
|
|
46226
46368
|
* default) builds it on the first expansion and keeps it afterwards.
|
|
46227
46369
|
* `"always"` builds it right away; in layout="column" it also gives the
|
|
46228
46370
|
* closed expandable its content's height (the content is kept laid out at
|
|
46229
46371
|
* its open width), so opening only reveals the width instead of changing the
|
|
46230
|
-
* height too. `"
|
|
46372
|
+
* height too. `"idle"` builds it in a browser idle moment after load.
|
|
46373
|
+
* `"while-opened"` throws the content away once the collapse
|
|
46231
46374
|
* settles — after the closing animation, so it still plays on real
|
|
46232
|
-
* content — and rebuilds it from scratch on every expansion.
|
|
46375
|
+
* content — and rebuilds it from scratch on every expansion. Whatever the
|
|
46376
|
+
* value, intent on the UI part (pointer entering it, focus landing in it)
|
|
46377
|
+
* builds the content ahead of the click.
|
|
46233
46378
|
*/
|
|
46234
46379
|
const Expandable = props => {
|
|
46235
46380
|
import.meta.css = [css$Q, "@jsenv/navi/src/control/expandable/expandable.jsx"];
|
|
@@ -46291,8 +46436,12 @@ const Expandable = props => {
|
|
|
46291
46436
|
// Fully open (or fully closed) and no longer moving — what releases the
|
|
46292
46437
|
// clipping, see the CSS.
|
|
46293
46438
|
const [settled, setSettled] = useState(true);
|
|
46294
|
-
const contentMounted = usePopupContentMount(openController, contentContainerRef,
|
|
46295
|
-
|
|
46439
|
+
const contentMounted = usePopupContentMount(openController, contentContainerRef,
|
|
46440
|
+
// The UI part is what the user aims at to expand — the closest thing an
|
|
46441
|
+
// expandable has to a popup's anchor, warming the content on intent.
|
|
46442
|
+
{
|
|
46443
|
+
mount,
|
|
46444
|
+
anchor: uiRef
|
|
46296
46445
|
});
|
|
46297
46446
|
|
|
46298
46447
|
// The pointer press that is about to toggle can blur the focused field
|
|
@@ -55275,6 +55424,15 @@ const SlideContainer = ({
|
|
|
55275
55424
|
,
|
|
55276
55425
|
|
|
55277
55426
|
"data-travel-by-drag": dragAxes ?? undefined
|
|
55427
|
+
// …and said as a listener too, from the same render: a touch this box may
|
|
55428
|
+
// take has to be refusable before the finger lands, and being registered
|
|
55429
|
+
// is the whole of it — see keepTouchRefusable for why a JSX prop is
|
|
55430
|
+
// enough here (an element-level touchmove listener is non-passive).
|
|
55431
|
+
// Without it, a swipe the box has taken can be cancelled mid-gesture by
|
|
55432
|
+
// the browser scrolling the page along the axis touch-action leaves it.
|
|
55433
|
+
,
|
|
55434
|
+
|
|
55435
|
+
onTouchMove: dragAxes ? keepTouchRefusable : undefined
|
|
55278
55436
|
// The same fact for a wheel, and only for that second reason: this box
|
|
55279
55437
|
// takes the push, whatever the box around it also travels on.
|
|
55280
55438
|
,
|
|
@@ -57950,15 +58108,19 @@ const css$E = /* css */`
|
|
|
57950
58108
|
* open controller (see `open_controller.js`) for a caller that wants to
|
|
57951
58109
|
* drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
|
|
57952
58110
|
* by `picker_custom.jsx`).
|
|
57953
|
-
* @param {"always"|"from-first-open"|"while-opened"} [props.mount] - When
|
|
58111
|
+
* @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
|
|
57954
58112
|
* `children` are built and thrown away (see popup_content_mount.js).
|
|
57955
58113
|
* `"from-first-open"` (the default) builds them on the first open and keeps
|
|
57956
58114
|
* them afterwards. `"always"` builds them right away, for content something
|
|
57957
58115
|
* depends on while the popup is still closed: a value read off it, fields a
|
|
57958
58116
|
* surrounding form collects on submit, a size measured from outside.
|
|
57959
|
-
* `"
|
|
57960
|
-
*
|
|
57961
|
-
*
|
|
58117
|
+
* `"idle"` builds them in a browser idle moment after load — "always" minus
|
|
58118
|
+
* the cost on the critical render. `"while-opened"` throws them away once
|
|
58119
|
+
* the popup has finished closing, for content whose fresh state is its
|
|
58120
|
+
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
58121
|
+
* changed while the popup was closed. Whatever the value, intent on the
|
|
58122
|
+
* anchor (pointer entering it, focus landing in it) builds the content
|
|
58123
|
+
* ahead of the click.
|
|
57962
58124
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
57963
58125
|
*/
|
|
57964
58126
|
const Dialog = props => {
|
|
@@ -58203,7 +58365,8 @@ const useDialogProps = props => {
|
|
|
58203
58365
|
// built: what this popup opens ON is known before anything reads it.
|
|
58204
58366
|
openController.onOpen = onOpen || null;
|
|
58205
58367
|
const contentMounted = usePopupContentMount(openController, props.ref, {
|
|
58206
|
-
mount
|
|
58368
|
+
mount,
|
|
58369
|
+
anchor
|
|
58207
58370
|
});
|
|
58208
58371
|
const children = contentMounted ? childrenProp : null;
|
|
58209
58372
|
const isModal = layer === "top";
|
|
@@ -59417,15 +59580,19 @@ const css$D = /* css */`
|
|
|
59417
59580
|
* open controller (see `open_controller.js`) for a caller that wants to
|
|
59418
59581
|
* drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
|
|
59419
59582
|
* by `picker_custom.jsx`/`side_panel.jsx`).
|
|
59420
|
-
* @param {"always"|"from-first-open"|"while-opened"} [props.mount] - When
|
|
59583
|
+
* @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
|
|
59421
59584
|
* `children` are built and thrown away (see popup_content_mount.js).
|
|
59422
59585
|
* `"from-first-open"` (the default) builds them on the first open and keeps
|
|
59423
59586
|
* them afterwards. `"always"` builds them right away, for content something
|
|
59424
59587
|
* depends on while the popup is still closed: a value read off it, fields a
|
|
59425
59588
|
* surrounding form collects on submit, a size measured from outside.
|
|
59426
|
-
* `"
|
|
59427
|
-
*
|
|
59428
|
-
*
|
|
59589
|
+
* `"idle"` builds them in a browser idle moment after load — "always" minus
|
|
59590
|
+
* the cost on the critical render. `"while-opened"` throws them away once
|
|
59591
|
+
* the popup has finished closing, for content whose fresh state is its
|
|
59592
|
+
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
59593
|
+
* changed while the popup was closed. Whatever the value, intent on the
|
|
59594
|
+
* anchor (pointer entering it, focus landing in it) builds the content
|
|
59595
|
+
* ahead of the click.
|
|
59429
59596
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
59430
59597
|
*/
|
|
59431
59598
|
const Popover = props => {
|
|
@@ -59648,7 +59815,8 @@ const usePopoverProps = props => {
|
|
|
59648
59815
|
// built: what this popup opens ON is known before anything reads it.
|
|
59649
59816
|
openController.onOpen = onOpen || null;
|
|
59650
59817
|
const contentMounted = usePopupContentMount(openController, props.ref, {
|
|
59651
|
-
mount
|
|
59818
|
+
mount,
|
|
59819
|
+
anchor
|
|
59652
59820
|
});
|
|
59653
59821
|
const children = contentMounted ? childrenProp : null;
|
|
59654
59822
|
const isTopLayer = layer === "top";
|
|
@@ -60588,7 +60756,7 @@ const css$C = /* css */`@layer navi {
|
|
|
60588
60756
|
* @param {string} [props.className] - Merged with the shared
|
|
60589
60757
|
* `"navi_popup"` class (see this file's own CSS) rather than replacing
|
|
60590
60758
|
* it.
|
|
60591
|
-
* @param {"always"|"from-first-open"|"while-opened"} [props.mount] - When
|
|
60759
|
+
* @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
|
|
60592
60760
|
* `children` are built and thrown away (see popup_content_mount.js).
|
|
60593
60761
|
* `"from-first-open"` (the default) builds them on the first open and keeps
|
|
60594
60762
|
* them afterwards. `"always"` builds them right away, for content something
|
|
@@ -60885,7 +61053,13 @@ const PickerCustom = props => {
|
|
|
60885
61053
|
// What a `--navi-confirm` said inside the popup means, once the popup has
|
|
60886
61054
|
// closed on it. A confirm picker is the one saying something (see
|
|
60887
61055
|
// picker_confirm.jsx): its press, deferred until the question is answered.
|
|
60888
|
-
onConfirm
|
|
61056
|
+
onConfirm,
|
|
61057
|
+
// The popup's lifecycle, the same pair Dialog and Popover take. Taken on
|
|
61058
|
+
// the picker and chained into its own openController below: the picker
|
|
61059
|
+
// hands the popup that controller, and a controlled Dialog/Popover reads
|
|
61060
|
+
// no onOpen/onClose of its own.
|
|
61061
|
+
onOpen,
|
|
61062
|
+
onClose
|
|
60889
61063
|
} = props;
|
|
60890
61064
|
// Resolve the id the same way useControlProps does (own id > Field's id > generated id)
|
|
60891
61065
|
// before computing popupId below, so two Pickers without an explicit id never collide.
|
|
@@ -60919,6 +61093,8 @@ const PickerCustom = props => {
|
|
|
60919
61093
|
delete pickerProps.defaultOpen;
|
|
60920
61094
|
delete pickerProps.escapeEffect;
|
|
60921
61095
|
delete pickerProps.onConfirm;
|
|
61096
|
+
delete pickerProps.onOpen;
|
|
61097
|
+
delete pickerProps.onClose;
|
|
60922
61098
|
// Read below for the popup alone; on the trigger it would land on the DOM as
|
|
60923
61099
|
// an unknown attribute holding a ref object.
|
|
60924
61100
|
delete pickerProps.anchor;
|
|
@@ -60927,6 +61103,13 @@ const PickerCustom = props => {
|
|
|
60927
61103
|
popupProps,
|
|
60928
61104
|
actionEvent: "custom"
|
|
60929
61105
|
});
|
|
61106
|
+
if (pickerProps.resetOnError === undefined) {
|
|
61107
|
+
// The picker's action fails after its popup closed on the value: nobody is
|
|
61108
|
+
// left mid-edit, so the value the server refused rolls back to the last
|
|
61109
|
+
// accepted one and the error callout says why — the same default
|
|
61110
|
+
// PickerNative takes.
|
|
61111
|
+
pickerProps.resetOnError = true;
|
|
61112
|
+
}
|
|
60930
61113
|
// ref
|
|
60931
61114
|
const popupRef = useRef(null);
|
|
60932
61115
|
popupProps.ref = popupRef;
|
|
@@ -60983,6 +61166,7 @@ const PickerCustom = props => {
|
|
|
60983
61166
|
// isUIStateHeld). Read at open, before anything inside can change it.
|
|
60984
61167
|
const heldAtOpen = isUIStateHeld(getPickerInput(ref.current)?.__uiStateController__);
|
|
60985
61168
|
debugPopup(openEvent, `picker opened, store value at open`, valueAtOpen, heldAtOpen ? `(held)` : `(a suggestion, not an answer yet)`);
|
|
61169
|
+
onOpen?.(openEvent);
|
|
60986
61170
|
return {
|
|
60987
61171
|
onRequestClose: requestCloseEvent => {
|
|
60988
61172
|
if (requestCloseEvent.detail.isCancel) {
|
|
@@ -61065,6 +61249,9 @@ const PickerCustom = props => {
|
|
|
61065
61249
|
if (confirmEvent && !closeEvent.detail.isCancel) {
|
|
61066
61250
|
onConfirm?.(confirmEvent);
|
|
61067
61251
|
}
|
|
61252
|
+
// Last, after the value bookkeeping above: whoever listens reads the
|
|
61253
|
+
// picker's value as it ends up — committed, or restored on a cancel.
|
|
61254
|
+
onClose?.(closeEvent);
|
|
61068
61255
|
}
|
|
61069
61256
|
};
|
|
61070
61257
|
});
|
|
@@ -82127,7 +82314,14 @@ const SidePanel = ({
|
|
|
82127
82314
|
,
|
|
82128
82315
|
|
|
82129
82316
|
"data-drag-travel": swipeToClose ? SWIPE_AXIS_BY_SIDE[side] : undefined,
|
|
82130
|
-
"data-travel-by-drag": swipeToClose ? SWIPE_AXIS_BY_SIDE[side] : undefined
|
|
82317
|
+
"data-travel-by-drag": swipeToClose ? SWIPE_AXIS_BY_SIDE[side] : undefined
|
|
82318
|
+
// A touch this panel may take has to be refusable before the finger
|
|
82319
|
+
// lands, or the browser can cancel the close gesture mid-swipe by
|
|
82320
|
+
// scrolling the panel's content — see keepTouchRefusable for why a JSX
|
|
82321
|
+
// prop is enough (an element-level touchmove listener is non-passive).
|
|
82322
|
+
,
|
|
82323
|
+
|
|
82324
|
+
onTouchMove: swipeToClose ? keepTouchRefusable : undefined,
|
|
82131
82325
|
...rest,
|
|
82132
82326
|
onPointerDown: pointerDownEvent => {
|
|
82133
82327
|
rest.onPointerDown?.(pointerDownEvent);
|