@jsenv/navi 0.29.324 → 0.29.326
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/README.md +1 -1
- package/dist/dev/jsenv_navi.js +247 -125
- package/dist/dev/jsenv_navi.js.map +11 -11
- package/dist/jsenv_navi.js +181 -129
- package/dist/jsenv_navi.js.map +11 -11
- package/docs/AI_INSTRUCTIONS.md +3 -2
- package/docs/dialog_shape.md +38 -18
- package/docs/drag_to_travel.md +3 -3
- package/docs/popup_backdrop.md +9 -7
- package/docs/popup_open.md +51 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ between related actions. Parent/child relations are first-class — `.one`,
|
|
|
29
29
|
|
|
30
30
|
**`Box`** is the main layout primitive. It wraps CSS Flexbox with a friendlier API: `flex` for horizontal layout, `flex="y"` for vertical (no more guessing what `flex-direction: column` does visually). Supports `grid`, `inline`, alignment via `alignX`/`alignY`, and spacing props.
|
|
31
31
|
|
|
32
|
-
**`createSlot`** renders content declared in one place at another: a toolbar, a status, or one `SidePanel` for a whole board
|
|
32
|
+
**`createSlot`** renders content declared in one place at another: a toolbar, a status, or one `SidePanel` for a whole board bound to the state that names what it shows (see [docs/popup_open.md](./docs/popup_open.md)).
|
|
33
33
|
|
|
34
34
|
**`Text`** and related components (`Title`, `Paragraph`, `Code`, `Caption`) handle typography consistently across the app — including the parts of a line that are not text: icons, counts, truncation (`maxLines`), skeletons, and where a line may break. See [docs/typography.md](./docs/typography.md).
|
|
35
35
|
|
package/dist/dev/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, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, getVirtualKeyboardOverlayHeight, 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, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createEventGroupLogger, createInternalCustomEvent, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, getVirtualKeyboardOverlayHeight, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
8
8
|
export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
|
|
9
9
|
import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
|
|
10
10
|
import { isValidElement, createContext, render, h, toChildArray, options, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
|
|
@@ -3235,6 +3235,15 @@ const resolveActionProxies = (actionSet) => {
|
|
|
3235
3235
|
|
|
3236
3236
|
const NO_PARAMS = { __no_params__: true };
|
|
3237
3237
|
const mergeActionParams = (currentParams, newParams) => {
|
|
3238
|
+
if (newParams === null) {
|
|
3239
|
+
// `null` is a question that cannot be asked: a callback destructuring its
|
|
3240
|
+
// params throws on it. It is what a computed says when it has not got
|
|
3241
|
+
// enough to ask with, and "nothing to ask" is said with `undefined`
|
|
3242
|
+
// everywhere below (and by the run guards reading paramsSignal), so it
|
|
3243
|
+
// becomes that here, at the one door params coming from a binding go
|
|
3244
|
+
// through.
|
|
3245
|
+
newParams = undefined;
|
|
3246
|
+
}
|
|
3238
3247
|
// The order of these two checks is load-bearing. Checking `undefined` first
|
|
3239
3248
|
// looks symmetric — "no new params, keep whatever is there, NO_PARAMS
|
|
3240
3249
|
// included" — and breaks routing: merge(NO_PARAMS, undefined) must yield
|
|
@@ -3492,7 +3501,10 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
3492
3501
|
*
|
|
3493
3502
|
* @param {any} newParamsOrSignal - params, or a signal holding them (the
|
|
3494
3503
|
* result then retargets itself as the signal changes), or an object whose
|
|
3495
|
-
* values may be signals.
|
|
3504
|
+
* values may be signals. A signal with nothing to ask yet — a question
|
|
3505
|
+
* computed from a form still missing a field — holds `undefined` or
|
|
3506
|
+
* `null`: the instance then has no params, which is what
|
|
3507
|
+
* `useAsyncData(action, { run: true })` reads to start nothing.
|
|
3496
3508
|
* @param {object} [options]
|
|
3497
3509
|
* @param {number} [options.debounce] - milliseconds the params must stay
|
|
3498
3510
|
* stable before the instance follows them. What it buys is a screen that
|
|
@@ -13207,6 +13219,12 @@ const useExecuteAction = (
|
|
|
13207
13219
|
// whose command sent — is stored as the callout display target, so the
|
|
13208
13220
|
// message appears on whoever asked rather than on the whole control.
|
|
13209
13221
|
const element = elementRef.current;
|
|
13222
|
+
if (!element) {
|
|
13223
|
+
// The control left the page while its run was out (its own answer can
|
|
13224
|
+
// unmount it, see the outcome events below): there is no box to hang
|
|
13225
|
+
// the refusal on. onActionError is still told, by the run's side effect.
|
|
13226
|
+
return;
|
|
13227
|
+
}
|
|
13210
13228
|
let target = requester;
|
|
13211
13229
|
// A requester that is no longer on the page is not a place to show
|
|
13212
13230
|
// anything: the clear cross leaves with the value it optimistically
|
|
@@ -13310,12 +13328,20 @@ const useExecuteAction = (
|
|
|
13310
13328
|
// Either three callbacks, one per outcome, or a single one for "however
|
|
13311
13329
|
// this ends" — it receives `{ aborted, reason }`, `{ error }` or
|
|
13312
13330
|
// `{ data }`, so a caller that only needs to know the action settled
|
|
13313
|
-
// (and whether it worked) does not have to register three.
|
|
13331
|
+
// (and whether it worked) does not have to register three. Each also
|
|
13332
|
+
// receives the outcome event (navi_action_abort/error/end) as its
|
|
13333
|
+
// second argument.
|
|
13314
13334
|
const addSideEffect = (sideEffect) => {
|
|
13315
13335
|
if (typeof sideEffect === "function") {
|
|
13316
|
-
addAbortCallback((reason) =>
|
|
13317
|
-
|
|
13318
|
-
|
|
13336
|
+
addAbortCallback((reason, outcomeEvent) =>
|
|
13337
|
+
sideEffect({ aborted: true, reason }, outcomeEvent),
|
|
13338
|
+
);
|
|
13339
|
+
addErrorCallback((error, outcomeEvent) =>
|
|
13340
|
+
sideEffect({ error }, outcomeEvent),
|
|
13341
|
+
);
|
|
13342
|
+
addCompleteCallback((data, outcomeEvent) =>
|
|
13343
|
+
sideEffect({ data }, outcomeEvent),
|
|
13344
|
+
);
|
|
13319
13345
|
return;
|
|
13320
13346
|
}
|
|
13321
13347
|
const { abort, error, complete } = sideEffect;
|
|
@@ -13323,56 +13349,6 @@ const useExecuteAction = (
|
|
|
13323
13349
|
addErrorCallback(error);
|
|
13324
13350
|
addCompleteCallback(complete);
|
|
13325
13351
|
};
|
|
13326
|
-
addSideEffect({
|
|
13327
|
-
abort: (reason) => {
|
|
13328
|
-
const element = elementRef.current;
|
|
13329
|
-
if (
|
|
13330
|
-
// at this stage the action side effect might have removed the <element> from the DOM
|
|
13331
|
-
// (in theory no because action side effect are batched to happen after)
|
|
13332
|
-
// but other side effects might do this
|
|
13333
|
-
element
|
|
13334
|
-
) {
|
|
13335
|
-
dispatchInternalCustomEvent(element, "navi_action_abort", {
|
|
13336
|
-
...sharedActionEventDetail,
|
|
13337
|
-
reason,
|
|
13338
|
-
});
|
|
13339
|
-
}
|
|
13340
|
-
},
|
|
13341
|
-
error: (error) => {
|
|
13342
|
-
if (errorEffect === "show_validation_message") {
|
|
13343
|
-
addErrorMessage(error, { requester });
|
|
13344
|
-
} else if (errorEffect === "throw") {
|
|
13345
|
-
setError(error);
|
|
13346
|
-
}
|
|
13347
|
-
|
|
13348
|
-
const element = elementRef.current;
|
|
13349
|
-
if (
|
|
13350
|
-
// at this stage the action side effect might have removed the <element> from the DOM
|
|
13351
|
-
// (in theory no because action side effect are batched to happen after)
|
|
13352
|
-
// but other side effects might do this
|
|
13353
|
-
element
|
|
13354
|
-
) {
|
|
13355
|
-
dispatchInternalCustomEvent(element, "navi_action_error", {
|
|
13356
|
-
...sharedActionEventDetail,
|
|
13357
|
-
error,
|
|
13358
|
-
});
|
|
13359
|
-
}
|
|
13360
|
-
},
|
|
13361
|
-
complete: (data) => {
|
|
13362
|
-
const element = elementRef.current;
|
|
13363
|
-
if (
|
|
13364
|
-
// at this stage the action side effect might have removed the <element> from the DOM
|
|
13365
|
-
// (in theory no because action side effect are batched to happen after)
|
|
13366
|
-
// but other side effects might do this
|
|
13367
|
-
element
|
|
13368
|
-
) {
|
|
13369
|
-
dispatchInternalCustomEvent(element, "navi_action_end", {
|
|
13370
|
-
...sharedActionEventDetail,
|
|
13371
|
-
data,
|
|
13372
|
-
});
|
|
13373
|
-
}
|
|
13374
|
-
},
|
|
13375
|
-
});
|
|
13376
13352
|
|
|
13377
13353
|
const actionStartEventDetail = {
|
|
13378
13354
|
...sharedActionEventDetail,
|
|
@@ -13384,13 +13360,50 @@ const useExecuteAction = (
|
|
|
13384
13360
|
actionStartEventDetail,
|
|
13385
13361
|
);
|
|
13386
13362
|
|
|
13363
|
+
// The outcome is told twice: to the element, when there still is one,
|
|
13364
|
+
// and to the side effects registered at navi_action_start, always. A
|
|
13365
|
+
// run can outlive its control — the run's own answer, written to a
|
|
13366
|
+
// store the parent reads to decide what it draws, unmounts the control
|
|
13367
|
+
// one microtask before the run settles — and the element is then no
|
|
13368
|
+
// place to dispatch to: Preact drops a detached element's listeners.
|
|
13369
|
+
// The side effects are the run's, not the element's, so the event
|
|
13370
|
+
// reaches them either way.
|
|
13371
|
+
const tellOutcome = (outcomeEventName, outcomeEventDetail, trigger) => {
|
|
13372
|
+
const outcomeEvent = createInternalCustomEvent(outcomeEventName, {
|
|
13373
|
+
...sharedActionEventDetail,
|
|
13374
|
+
...outcomeEventDetail,
|
|
13375
|
+
});
|
|
13376
|
+
const element = elementRef.current;
|
|
13377
|
+
if (element) {
|
|
13378
|
+
element.dispatchEvent(outcomeEvent);
|
|
13379
|
+
}
|
|
13380
|
+
trigger(outcomeEvent);
|
|
13381
|
+
};
|
|
13382
|
+
|
|
13387
13383
|
const runAction = () => {
|
|
13388
13384
|
return action[method]({
|
|
13389
13385
|
event: actionEvent,
|
|
13390
13386
|
reason: `"${event.type}" event on ${getElementSignature(event.target)}`,
|
|
13391
|
-
onAbort:
|
|
13392
|
-
|
|
13393
|
-
|
|
13387
|
+
onAbort: (reason) => {
|
|
13388
|
+
tellOutcome("navi_action_abort", { reason }, (outcomeEvent) => {
|
|
13389
|
+
triggerAbort(reason, outcomeEvent);
|
|
13390
|
+
});
|
|
13391
|
+
},
|
|
13392
|
+
onError: (error) => {
|
|
13393
|
+
if (errorEffect === "show_validation_message") {
|
|
13394
|
+
addErrorMessage(error, { requester });
|
|
13395
|
+
} else if (errorEffect === "throw") {
|
|
13396
|
+
setError(error);
|
|
13397
|
+
}
|
|
13398
|
+
tellOutcome("navi_action_error", { error }, (outcomeEvent) => {
|
|
13399
|
+
triggerError(error, outcomeEvent);
|
|
13400
|
+
});
|
|
13401
|
+
},
|
|
13402
|
+
onComplete: (data) => {
|
|
13403
|
+
tellOutcome("navi_action_end", { data }, (outcomeEvent) => {
|
|
13404
|
+
triggerComplete(data, outcomeEvent);
|
|
13405
|
+
});
|
|
13406
|
+
},
|
|
13394
13407
|
});
|
|
13395
13408
|
};
|
|
13396
13409
|
|
|
@@ -17100,6 +17113,11 @@ const generateSignalId = () => {
|
|
|
17100
17113
|
* as the user moved. `"push"` is for a state whose values ARE places one came
|
|
17101
17114
|
* from (the photo one is looking at in a gallery). Whatever is said here, one
|
|
17102
17115
|
* write can say otherwise: `signal.set(value, { history })`.
|
|
17116
|
+
* Either way every write reaches the address, synchronously, and browsers
|
|
17117
|
+
* refuse an address written too often (Safari: 100 writes per 10 s, then a
|
|
17118
|
+
* SecurityError). A value one DRAGS is written at 60 Hz: keep it in the
|
|
17119
|
+
* gesture while the finger is down and write the state once, on release —
|
|
17120
|
+
* an address is not a recording of a gesture.
|
|
17103
17121
|
* @param {boolean} [options.debug=false] - Enable debug logging for this signal's operations
|
|
17104
17122
|
* @returns {import("@preact/signals").Signal} A signal that can be synchronized with a source signal and/or persisted in localStorage. The signal includes a `validity` property for validation state.
|
|
17105
17123
|
*
|
|
@@ -27293,11 +27311,7 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
27293
27311
|
[NAV_DEPTH_STATE_KEY]: getNavDepth(),
|
|
27294
27312
|
},
|
|
27295
27313
|
});
|
|
27296
|
-
|
|
27297
|
-
window.history.pushState(effectiveState, null, url);
|
|
27298
|
-
} else {
|
|
27299
|
-
window.history.replaceState(effectiveState, null, url);
|
|
27300
|
-
}
|
|
27314
|
+
writeHistoryEntry(navigationType, effectiveState, url);
|
|
27301
27315
|
rememberEntryIsOfThisDocument();
|
|
27302
27316
|
updateDocumentUrl(url);
|
|
27303
27317
|
updateDocumentState(effectiveState);
|
|
@@ -27317,14 +27331,14 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
27317
27331
|
[NAV_DEPTH_STATE_KEY]: getNavDepth(),
|
|
27318
27332
|
},
|
|
27319
27333
|
});
|
|
27320
|
-
|
|
27334
|
+
writeHistoryEntry("replace", state, url);
|
|
27321
27335
|
rememberEntryIsOfThisDocument();
|
|
27322
27336
|
} else if (redirected) {
|
|
27323
27337
|
// The entry the browser is on names an address that only sends
|
|
27324
27338
|
// elsewhere — a cold load on it, or a back into it. Written over where
|
|
27325
27339
|
// it stands (the entry keeps its place in the stack, hence its state
|
|
27326
27340
|
// and the depth in it): pressing back must not walk into it again.
|
|
27327
|
-
|
|
27341
|
+
writeHistoryEntry("replace", state, url);
|
|
27328
27342
|
rememberEntryIsOfThisDocument();
|
|
27329
27343
|
}
|
|
27330
27344
|
updateDocumentUrl(url);
|
|
@@ -27577,7 +27591,7 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
27577
27591
|
// The entry itself has to lose them too, not just the document state:
|
|
27578
27592
|
// getDocumentState() reads the entry back, and every state write copies
|
|
27579
27593
|
// what it finds there onto the next one.
|
|
27580
|
-
|
|
27594
|
+
writeHistoryEntry("replace", state, url);
|
|
27581
27595
|
}
|
|
27582
27596
|
handleRoutingTask(url, {
|
|
27583
27597
|
reason: "routing initialization",
|
|
@@ -27604,6 +27618,57 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
27604
27618
|
// replace onto the url already displayed. Never a traverse — the browser has
|
|
27605
27619
|
// already moved window.location.href when a popstate is handled, so the
|
|
27606
27620
|
// comparison would say "same" about every back and forward.
|
|
27621
|
+
// Browsers refuse an address written too often: WebKit throws a SecurityError
|
|
27622
|
+
// past 100 writes per 10 seconds, Chromium and Firefox drop the write. A state
|
|
27623
|
+
// bound to the url and written per frame gets there in under two seconds, and
|
|
27624
|
+
// a developer working in Chromium never sees it. Every write goes through here
|
|
27625
|
+
// so the rate is measured against the strictest budget before any browser
|
|
27626
|
+
// refuses, and so a refusal names its cause rather than `replaceState`.
|
|
27627
|
+
const HISTORY_WRITE_BUDGET = 100;
|
|
27628
|
+
const HISTORY_WRITE_WINDOW_MS = 10_000;
|
|
27629
|
+
const ADDRESS_WRITE_ADVICE = `An address is not a recording of a gesture: a value dragged per frame belongs to the gesture, and the state bound to the url is written once, on release.`;
|
|
27630
|
+
const writeHistoryEntry = (navigationType, state, url) => {
|
|
27631
|
+
noteHistoryWrite(url);
|
|
27632
|
+
try {
|
|
27633
|
+
if (navigationType === "push") {
|
|
27634
|
+
window.history.pushState(state, null, url);
|
|
27635
|
+
} else {
|
|
27636
|
+
window.history.replaceState(state, null, url);
|
|
27637
|
+
}
|
|
27638
|
+
} catch (e) {
|
|
27639
|
+
if (e.name !== "SecurityError") {
|
|
27640
|
+
throw e;
|
|
27641
|
+
}
|
|
27642
|
+
throw new Error(
|
|
27643
|
+
`The browser refused to write the address ${url}: ${e.message}. ${ADDRESS_WRITE_ADVICE}`,
|
|
27644
|
+
{ cause: e },
|
|
27645
|
+
);
|
|
27646
|
+
}
|
|
27647
|
+
};
|
|
27648
|
+
const noteHistoryWrite = (() => {
|
|
27649
|
+
const writeTimes = [];
|
|
27650
|
+
let warned = false;
|
|
27651
|
+
return (url) => {
|
|
27652
|
+
if (warned) {
|
|
27653
|
+
return;
|
|
27654
|
+
}
|
|
27655
|
+
const now = performance.now();
|
|
27656
|
+
writeTimes.push(now);
|
|
27657
|
+
while (writeTimes[0] < now - HISTORY_WRITE_WINDOW_MS) {
|
|
27658
|
+
writeTimes.shift();
|
|
27659
|
+
}
|
|
27660
|
+
if (writeTimes.length < HISTORY_WRITE_BUDGET) {
|
|
27661
|
+
return;
|
|
27662
|
+
}
|
|
27663
|
+
warned = true;
|
|
27664
|
+
const seconds = ((now - writeTimes[0]) / 1000).toFixed(1);
|
|
27665
|
+
console.warn(
|
|
27666
|
+
`[navi] the address was written ${HISTORY_WRITE_BUDGET} times in ${seconds}s (last: ${url}). Safari refuses more than ${HISTORY_WRITE_BUDGET} writes per ${HISTORY_WRITE_WINDOW_MS / 1000}s with a SecurityError, and the state then runs ahead of the address. ${ADDRESS_WRITE_ADVICE}`,
|
|
27667
|
+
);
|
|
27668
|
+
};
|
|
27669
|
+
})()
|
|
27670
|
+
;
|
|
27671
|
+
|
|
27607
27672
|
const isStateOnlyNavigation = (
|
|
27608
27673
|
url,
|
|
27609
27674
|
{ navigationType },
|
|
@@ -40910,10 +40975,24 @@ const useInteractiveProps = (props, {
|
|
|
40910
40975
|
// aborted run, whose promise is awaited to completion (see
|
|
40911
40976
|
// performRun in actions.js) — which is exactly what "the server is
|
|
40912
40977
|
// done with it" means, and therefore when the queued request may go.
|
|
40913
|
-
e.detail.addSideEffect(outcome => {
|
|
40978
|
+
e.detail.addSideEffect((outcome, outcomeEvent) => {
|
|
40914
40979
|
uiStateController.actionInFlight = false;
|
|
40915
40980
|
uiStateController.runningActionSignal.value = null;
|
|
40916
40981
|
uiStateController.parallelGuard?.release(uiStateController);
|
|
40982
|
+
// The outcome callbacks are told from here, the run's side effect,
|
|
40983
|
+
// and not from the onnavi_action_* handlers below: the run can
|
|
40984
|
+
// outlive this control (its own answer, written to a store the
|
|
40985
|
+
// parent reads, unmounts it one microtask before it settles), and
|
|
40986
|
+
// whoever wears the run — a parent's `loading` following these,
|
|
40987
|
+
// see 12_picker_card_demo.html — must hear it end all the same.
|
|
40988
|
+
// The handlers below keep what needs the element.
|
|
40989
|
+
if (outcome.aborted) {
|
|
40990
|
+
onActionAborted?.(outcomeEvent);
|
|
40991
|
+
} else if ("error" in outcome) {
|
|
40992
|
+
onActionError?.(outcome.error, outcomeEvent);
|
|
40993
|
+
} else {
|
|
40994
|
+
onActionEnd?.(outcome.data, outcomeEvent);
|
|
40995
|
+
}
|
|
40917
40996
|
const queuedEvent = uiStateController.queuedActionAllowedEvent;
|
|
40918
40997
|
if (!queuedEvent) {
|
|
40919
40998
|
syncOptimisticAttribute(false);
|
|
@@ -40953,7 +41032,6 @@ const useInteractiveProps = (props, {
|
|
|
40953
41032
|
if (resetOnAbort && !superseded) {
|
|
40954
41033
|
dispatchRequestResetUIState(e.currentTarget, e);
|
|
40955
41034
|
}
|
|
40956
|
-
onActionAborted?.(e);
|
|
40957
41035
|
},
|
|
40958
41036
|
onnavi_action_error: e => {
|
|
40959
41037
|
const {
|
|
@@ -40963,7 +41041,6 @@ const useInteractiveProps = (props, {
|
|
|
40963
41041
|
if (resetOnError) {
|
|
40964
41042
|
dispatchRequestResetUIState(e.currentTarget, e);
|
|
40965
41043
|
}
|
|
40966
|
-
onActionError?.(error, e);
|
|
40967
41044
|
uiStateController.onActionError(e);
|
|
40968
41045
|
},
|
|
40969
41046
|
onnavi_action_end: e => {
|
|
@@ -40971,7 +41048,6 @@ const useInteractiveProps = (props, {
|
|
|
40971
41048
|
data
|
|
40972
41049
|
} = e.detail;
|
|
40973
41050
|
debugAction(e, `action end with data: ${JSON.stringify(data)}`);
|
|
40974
|
-
onActionEnd?.(data, e);
|
|
40975
41051
|
controlRootProps.onnavi_action_end?.(e);
|
|
40976
41052
|
uiStateController.onActionEnd(e);
|
|
40977
41053
|
|
|
@@ -60451,34 +60527,44 @@ const css$E = /* css */`
|
|
|
60451
60527
|
* `showModal()`'d (native focus trap, `Escape`-to-cancel, hardware
|
|
60452
60528
|
* back-button dismissal, rest-of-document made inert), without one it is
|
|
60453
60529
|
* shown through the Popover API and the page behind stays live.
|
|
60454
|
-
* @param {boolean} [props.dockedOnSmallTouchScreen] - Turns
|
|
60455
|
-
*
|
|
60456
|
-
* screen, and leaves it alone otherwise
|
|
60457
|
-
*
|
|
60458
|
-
* the
|
|
60459
|
-
*
|
|
60460
|
-
*
|
|
60461
|
-
*
|
|
60462
|
-
*
|
|
60463
|
-
*
|
|
60464
|
-
*
|
|
60465
|
-
*
|
|
60530
|
+
* @param {boolean|"top"|"bottom"} [props.dockedOnSmallTouchScreen] - Turns
|
|
60531
|
+
* the dialog into a sheet (docked flush to one edge, full width) on a small
|
|
60532
|
+
* touch screen, and leaves it alone otherwise: under a mouse the centered box
|
|
60533
|
+
* is already the right shape, while on a phone a centered box ends up cramped
|
|
60534
|
+
* between the keyboard and the top of the screen. The value says which edge
|
|
60535
|
+
* the sheet rests on. `true` (or `"top"`) rests it on the top edge, out of
|
|
60536
|
+
* the virtual keyboard's way: a field tapped in the sheet raises the keyboard
|
|
60537
|
+
* below it, and the question and the field stay where the finger found them.
|
|
60538
|
+
* `"bottom"` rests it on the bottom edge, where the thumbs are — for a sheet
|
|
60539
|
+
* that is read and tapped (a list, a couple of buttons, a confirmation) and
|
|
60540
|
+
* holds nothing that raises a keyboard; one that does is reflowed into the
|
|
60541
|
+
* strip left above the keyboard at every keystroke. The edge is said here
|
|
60542
|
+
* rather than through `positionArea`, which places both shapes:
|
|
60543
|
+
* `positionArea="bottom"` would also pin the centered box to the bottom of a
|
|
60544
|
+
* desktop window. Both halves of the name matter (`smallTouchScreenSignal`):
|
|
60545
|
+
* touch alone would dock a big touch screen — a tablet, a kiosk panel — a
|
|
60546
|
+
* whole screen away from where the finger just tapped, and size alone would
|
|
60547
|
+
* dock a narrow desktop window, which is still a mouse. It supplies defaults
|
|
60548
|
+
* for `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`,
|
|
60549
|
+
* so any of them can still be pinned explicitly — including `expandX={false}`,
|
|
60466
60550
|
* which opts the docked dialog out of the full-width stretch and leaves it a
|
|
60467
|
-
* floating box
|
|
60551
|
+
* floating box against its edge. It also withdraws `maxWidth` while docked: a
|
|
60468
60552
|
* sheet is container-wide by definition, and a `maxWidth` is an answer about
|
|
60469
60553
|
* the *centered* shape, so the two can be stated together (`maxWidth="16rem"
|
|
60470
60554
|
* dockedOnSmallTouchScreen`) and each applies where it means something.
|
|
60471
60555
|
* `minWidth` needs no such rule — its floor is below the full width — and
|
|
60472
60556
|
* `maxHeight`/`minHeight` keep applying, a sheet being content-tall.
|
|
60473
|
-
* Ignored entirely when `expandY`
|
|
60474
|
-
*
|
|
60475
|
-
*
|
|
60476
|
-
*
|
|
60477
|
-
*
|
|
60478
|
-
*
|
|
60479
|
-
*
|
|
60480
|
-
*
|
|
60481
|
-
*
|
|
60557
|
+
* Ignored entirely when `expandY` (or `expand`) is set: a dialog already
|
|
60558
|
+
* filling the height touches both edges, so docking could only take away the
|
|
60559
|
+
* shape the caller asked for. Re-resolves live as the pointer type or the
|
|
60560
|
+
* window size changes. A sheet is also pushed back through the edge it rests
|
|
60561
|
+
* on to close it (down for a bottom sheet, up for a top one), held by its
|
|
60562
|
+
* header (a `Box` with the `header` prop) and by anything else carrying
|
|
60563
|
+
* `data-swipe-grip`. The rest of the sheet is left to what it holds, so a
|
|
60564
|
+
* board something is dragged across keeps its own gestures. See
|
|
60565
|
+
* `swipe_to_close.js`. In dev, a `"bottom"` sheet found holding a field that
|
|
60566
|
+
* raises the keyboard is warned about at open, on any screen — the mistake
|
|
60567
|
+
* itself only shows on a phone.
|
|
60482
60568
|
* @param {string} [props.positionArea="center"] - Where to dock the dialog
|
|
60483
60569
|
* within its container (the viewport for `layer="top"`, the positioned
|
|
60484
60570
|
* ancestor for `layer="local"`) — Dialog is never anchored to a real
|
|
@@ -60846,28 +60932,28 @@ const DialogLocal = props => {
|
|
|
60846
60932
|
* own to render: a modal's is the native `::backdrop`, and `backdrop={false}`
|
|
60847
60933
|
* has none at all.
|
|
60848
60934
|
*/
|
|
60849
|
-
// What a dialog turns into on a small touch screen
|
|
60850
|
-
//
|
|
60851
|
-
//
|
|
60852
|
-
//
|
|
60935
|
+
// What a dialog turns into on a small touch screen: a sheet flush against one
|
|
60936
|
+
// edge of the screen. Which edge is the caller's word (the prop's value, see
|
|
60937
|
+
// its JSDoc), the top one unless told "bottom": the cheap mistake must be the
|
|
60938
|
+
// default one — a sheet docked top by mistake is a little farther from the
|
|
60939
|
+
// thumb, a sheet docked bottom by mistake fights the virtual keyboard, which
|
|
60940
|
+
// rises from that very edge, on every keystroke.
|
|
60853
60941
|
// Only defaults: an explicitly passed prop still wins, so the docked shape can
|
|
60854
60942
|
// be adjusted one axis at a time instead of being all-or-nothing.
|
|
60855
60943
|
const DOCKED = {
|
|
60856
|
-
positionArea: "bottom",
|
|
60857
60944
|
marginWithContainer: 0,
|
|
60858
60945
|
expandX: true,
|
|
60859
|
-
// A sheet
|
|
60860
|
-
//
|
|
60861
|
-
// reasoning as "bottom" above, applied to the gesture instead of the shape.
|
|
60946
|
+
// A sheet is dragged with a thumb, and a drag that runs past its own edge
|
|
60947
|
+
// must not land on the page behind it.
|
|
60862
60948
|
scrollCapture: true
|
|
60863
60949
|
};
|
|
60864
60950
|
|
|
60865
|
-
// Where a
|
|
60866
|
-
// with `header`, plus anything the application marked as one more.
|
|
60867
|
-
// else in the sheet is content the finger came to operate — a board
|
|
60868
|
-
// dragged across, a list, a map — and a press there belongs to it. A
|
|
60869
|
-
// no header and nothing marked is not pushed
|
|
60870
|
-
// own controls, by the backdrop and by Escape.
|
|
60951
|
+
// Where a sheet is held to push it back through its edge: the strip a Box
|
|
60952
|
+
// declares with `header`, plus anything the application marked as one more.
|
|
60953
|
+
// Everything else in the sheet is content the finger came to operate — a board
|
|
60954
|
+
// a piece is dragged across, a list, a map — and a press there belongs to it. A
|
|
60955
|
+
// sheet with no header and nothing marked is not pushed back at all; it is
|
|
60956
|
+
// closed by its own controls, by the backdrop and by Escape.
|
|
60871
60957
|
const DOCKED_SWIPE_GRIP = "[data-header],[data-swipe-grip]";
|
|
60872
60958
|
const useDialogProps = props => {
|
|
60873
60959
|
const backdropProps = {};
|
|
@@ -60973,12 +61059,15 @@ const useDialogProps = props => {
|
|
|
60973
61059
|
// Only a small touch screen changes anything: on a mouse — and on a touch
|
|
60974
61060
|
// screen too big to reach the bottom edge of — a dialog already wants to be
|
|
60975
61061
|
// the centered box it is by default, so there is nothing to resolve.
|
|
60976
|
-
// expandY cancels the docking outright:
|
|
60977
|
-
//
|
|
60978
|
-
//
|
|
60979
|
-
// caller asked for (and arm a swipe-down on something that never rose).
|
|
61062
|
+
// expandY cancels the docking outright: a dialog already filling the height
|
|
61063
|
+
// touches both edges — all docking could still do is take away the shape
|
|
61064
|
+
// the caller asked for (and arm a swipe on something that never rose).
|
|
60980
61065
|
const isDocked = dockedOnSmallTouchScreen && smallTouchScreenSignal.value && !expandY;
|
|
60981
|
-
const
|
|
61066
|
+
const dockedEdge = dockedOnSmallTouchScreen === "bottom" ? "bottom" : "top";
|
|
61067
|
+
const positionArea = positionAreaProp ?? (isDocked ? dockedEdge : "center");
|
|
61068
|
+
// Read off the shape a phone would give, not the one this screen gives: the
|
|
61069
|
+
// author of a bottom sheet tests on a desktop, where the mistake is silent.
|
|
61070
|
+
const sheetRestsOnBottom = Boolean(dockedOnSmallTouchScreen) && !expandY && restsOnBottom(positionAreaProp ?? dockedEdge);
|
|
60982
61071
|
const marginWithContainer = marginWithContainerProp ?? (isDocked ? DOCKED.marginWithContainer :
|
|
60983
61072
|
// A share of whatever holds the dialog: the app's own screen for a
|
|
60984
61073
|
// top-layer one — where appw is exactly "3% of the container", the
|
|
@@ -61040,12 +61129,12 @@ const useDialogProps = props => {
|
|
|
61040
61129
|
y: "center",
|
|
61041
61130
|
x: "center"
|
|
61042
61131
|
};
|
|
61043
|
-
// Pushing the sheet back
|
|
61044
|
-
//
|
|
61045
|
-
// actually resting on
|
|
61046
|
-
//
|
|
61047
|
-
const
|
|
61048
|
-
const onSwipePointerDown =
|
|
61132
|
+
// Pushing the sheet back through the edge it rests on closes it — the way
|
|
61133
|
+
// it came in, and the way the thumb on that edge already pushes. Only a sheet
|
|
61134
|
+
// actually resting on an edge: anywhere else the gesture would send the
|
|
61135
|
+
// dialog somewhere it never came from.
|
|
61136
|
+
const swipeToCloseSide = isDocked && (parsedPositionArea.y === "top" || parsedPositionArea.y === "bottom") ? parsedPositionArea.y : null;
|
|
61137
|
+
const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose(swipeToCloseSide, {
|
|
61049
61138
|
grip: DOCKED_SWIPE_GRIP
|
|
61050
61139
|
}) : null;
|
|
61051
61140
|
// A corner sitting exactly on the container's own corner must not be
|
|
@@ -61173,6 +61262,9 @@ const useDialogProps = props => {
|
|
|
61173
61262
|
}
|
|
61174
61263
|
{
|
|
61175
61264
|
warnAboutUnreachableOutsideRegions(dialogEl);
|
|
61265
|
+
if (sheetRestsOnBottom) {
|
|
61266
|
+
warnBottomSheetHoldingKeyboardControl(dialogEl);
|
|
61267
|
+
}
|
|
61176
61268
|
}
|
|
61177
61269
|
|
|
61178
61270
|
// Set by useOpenControllerByProps for the very first open triggered by
|
|
@@ -61721,12 +61813,12 @@ const useDialogProps = props => {
|
|
|
61721
61813
|
"data-flush-right": flushEdges.right ? "" : undefined,
|
|
61722
61814
|
"data-flush-bottom": flushEdges.bottom ? "" : undefined,
|
|
61723
61815
|
"data-flush-left": flushEdges.left ? "" : undefined,
|
|
61724
|
-
"data-swipe-to-close":
|
|
61816
|
+
"data-swipe-to-close": swipeToCloseSide ?? undefined,
|
|
61725
61817
|
// The axis the sheet travels on when it is pushed back, said to the shared
|
|
61726
61818
|
// gesture layer: it is what a box travelling inside the sheet reads to know
|
|
61727
61819
|
// this axis is already walked (see @jsenv/dom's drag_to_travel).
|
|
61728
|
-
"data-drag-travel":
|
|
61729
|
-
"data-travel-by-drag":
|
|
61820
|
+
"data-drag-travel": swipeToCloseSide ? SWIPE_AXIS_BY_SIDE[swipeToCloseSide] : undefined,
|
|
61821
|
+
"data-travel-by-drag": swipeToCloseSide ? SWIPE_AXIS_BY_SIDE[swipeToCloseSide] : undefined,
|
|
61730
61822
|
"onPointerDown": e => {
|
|
61731
61823
|
rest.onPointerDown?.(e);
|
|
61732
61824
|
onSwipePointerDown?.(e);
|
|
@@ -61798,6 +61890,33 @@ const useDialogProps = props => {
|
|
|
61798
61890
|
}
|
|
61799
61891
|
return [!isTopLayer && backdrop ? backdropProps : null, contentProps];
|
|
61800
61892
|
};
|
|
61893
|
+
const restsOnBottom = positionArea => {
|
|
61894
|
+
const parsed = parsePositionArea(positionArea);
|
|
61895
|
+
return Boolean(parsed) && parsed.y === "bottom";
|
|
61896
|
+
};
|
|
61897
|
+
|
|
61898
|
+
// What raises a virtual keyboard when tapped: a text-like input, a textarea,
|
|
61899
|
+
// anything editable. Not a `select` — it opens a menu, which takes no screen
|
|
61900
|
+
// from the sheet — and not a read-only field.
|
|
61901
|
+
const KEYBOARD_RAISING_CONTROL_SELECTOR = `
|
|
61902
|
+
input:not([type]):not([readonly]),
|
|
61903
|
+
input[type="text"]:not([readonly]),
|
|
61904
|
+
input[type="search"]:not([readonly]),
|
|
61905
|
+
input[type="email"]:not([readonly]),
|
|
61906
|
+
input[type="url"]:not([readonly]),
|
|
61907
|
+
input[type="tel"]:not([readonly]),
|
|
61908
|
+
input[type="password"]:not([readonly]),
|
|
61909
|
+
input[type="number"]:not([readonly]),
|
|
61910
|
+
textarea:not([readonly]),
|
|
61911
|
+
[contenteditable]:not([contenteditable="false"])
|
|
61912
|
+
`;
|
|
61913
|
+
const warnBottomSheetHoldingKeyboardControl = dialogEl => {
|
|
61914
|
+
const controlEl = dialogEl.querySelector(KEYBOARD_RAISING_CONTROL_SELECTOR);
|
|
61915
|
+
if (!controlEl) {
|
|
61916
|
+
return;
|
|
61917
|
+
}
|
|
61918
|
+
console.warn(`[navi] a Dialog docked to the bottom edge (dockedOnSmallTouchScreen="bottom") holds a field that raises the virtual keyboard. On a phone the keyboard rises from the edge the sheet rests on and reflows the sheet into the strip left above it, at every keystroke. A sheet one types into rests on the top edge: dockedOnSmallTouchScreen without a value. "bottom" is for a sheet one reads and taps.`, controlEl);
|
|
61919
|
+
};
|
|
61801
61920
|
const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
|
|
61802
61921
|
|
|
61803
61922
|
// Lets consumers pass animationDuration="0.5s" as a regular prop; Box maps
|
|
@@ -63529,8 +63648,8 @@ const css$C = /* css */`@layer navi {
|
|
|
63529
63648
|
* @param {string} [props.positionAreaFixed] - **Popover-only**, same guard.
|
|
63530
63649
|
* @param {string} [props.positionAreaWhenAnchorIsInvalid] - **Popover-only**,
|
|
63531
63650
|
* same guard.
|
|
63532
|
-
* @param {boolean} [props.dockedOnSmallTouchScreen] -
|
|
63533
|
-
* popover is never a
|
|
63651
|
+
* @param {boolean|"top"|"bottom"} [props.dockedOnSmallTouchScreen] -
|
|
63652
|
+
* **Dialog-only** (a popover is never a sheet), destructured out for the same reason
|
|
63534
63653
|
* turned around: it must not land on the popover element as a stray DOM
|
|
63535
63654
|
* attribute when the screen-size resolution picks `mode="popover"`. Where
|
|
63536
63655
|
* that resolution lands is what decides whether it applies at all, so a
|
|
@@ -63558,7 +63677,7 @@ const css$C = /* css */`@layer navi {
|
|
|
63558
63677
|
* honoured in either mode — a wall-less `Dialog` is shown through the
|
|
63559
63678
|
* Popover API rather than `showModal()` — so which mode the screen-size
|
|
63560
63679
|
* resolution picks says nothing about whether the page behind stays live.
|
|
63561
|
-
* It is also what makes a sheet docked to
|
|
63680
|
+
* It is also what makes a sheet docked to an edge of a phone's screen
|
|
63562
63681
|
* (`dockedOnSmallTouchScreen`) non-modal.
|
|
63563
63682
|
* @param {"auto"|"discrete"|"invisible"} [props.backdropVariant] - Forwarded
|
|
63564
63683
|
* as-is to whichever component renders (both understand it identically):
|
|
@@ -76813,7 +76932,7 @@ const css$n = /* css */`.navi_split_button {
|
|
|
76813
76932
|
* dialogExpand?: boolean,
|
|
76814
76933
|
* dialogExpandX?: boolean,
|
|
76815
76934
|
* dialogExpandY?: boolean,
|
|
76816
|
-
* dockedOnSmallTouchScreen?: boolean,
|
|
76935
|
+
* dockedOnSmallTouchScreen?: boolean | "top" | "bottom",
|
|
76817
76936
|
* marginWithContainer?: number | string,
|
|
76818
76937
|
* backdrop?: boolean,
|
|
76819
76938
|
* backdropVariant?: "auto" | "discrete" | "invisible",
|
|
@@ -86249,6 +86368,9 @@ const createSlot = (SlotRenderer = Box) => {
|
|
|
86249
86368
|
slotPropsSignal.value = props;
|
|
86250
86369
|
useLayoutEffect(() => {
|
|
86251
86370
|
return () => {
|
|
86371
|
+
// Within one diff preact mounts the newcomer before unmounting the
|
|
86372
|
+
// leaver, so the leaver may no longer be the filler by the time it
|
|
86373
|
+
// is cleaned up.
|
|
86252
86374
|
if (filler !== fillerRef) {
|
|
86253
86375
|
return;
|
|
86254
86376
|
}
|