@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.
@@ -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, 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, 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, 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, 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";
@@ -3092,6 +3092,15 @@ const resolveActionProxies = (actionSet) => {
3092
3092
 
3093
3093
  const NO_PARAMS = { __no_params__: true };
3094
3094
  const mergeActionParams = (currentParams, newParams) => {
3095
+ if (newParams === null) {
3096
+ // `null` is a question that cannot be asked: a callback destructuring its
3097
+ // params throws on it. It is what a computed says when it has not got
3098
+ // enough to ask with, and "nothing to ask" is said with `undefined`
3099
+ // everywhere below (and by the run guards reading paramsSignal), so it
3100
+ // becomes that here, at the one door params coming from a binding go
3101
+ // through.
3102
+ newParams = undefined;
3103
+ }
3095
3104
  // The order of these two checks is load-bearing. Checking `undefined` first
3096
3105
  // looks symmetric — "no new params, keep whatever is there, NO_PARAMS
3097
3106
  // included" — and breaks routing: merge(NO_PARAMS, undefined) must yield
@@ -3349,7 +3358,10 @@ const createAction = (callback, rootOptions = {}) => {
3349
3358
  *
3350
3359
  * @param {any} newParamsOrSignal - params, or a signal holding them (the
3351
3360
  * result then retargets itself as the signal changes), or an object whose
3352
- * values may be signals.
3361
+ * values may be signals. A signal with nothing to ask yet — a question
3362
+ * computed from a form still missing a field — holds `undefined` or
3363
+ * `null`: the instance then has no params, which is what
3364
+ * `useAsyncData(action, { run: true })` reads to start nothing.
3353
3365
  * @param {object} [options]
3354
3366
  * @param {number} [options.debounce] - milliseconds the params must stay
3355
3367
  * stable before the instance follows them. What it buys is a screen that
@@ -12616,6 +12628,12 @@ const useExecuteAction = (
12616
12628
  // whose command sent — is stored as the callout display target, so the
12617
12629
  // message appears on whoever asked rather than on the whole control.
12618
12630
  const element = elementRef.current;
12631
+ if (!element) {
12632
+ // The control left the page while its run was out (its own answer can
12633
+ // unmount it, see the outcome events below): there is no box to hang
12634
+ // the refusal on. onActionError is still told, by the run's side effect.
12635
+ return;
12636
+ }
12619
12637
  let target = requester;
12620
12638
  // A requester that is no longer on the page is not a place to show
12621
12639
  // anything: the clear cross leaves with the value it optimistically
@@ -12718,12 +12736,20 @@ const useExecuteAction = (
12718
12736
  // Either three callbacks, one per outcome, or a single one for "however
12719
12737
  // this ends" — it receives `{ aborted, reason }`, `{ error }` or
12720
12738
  // `{ data }`, so a caller that only needs to know the action settled
12721
- // (and whether it worked) does not have to register three.
12739
+ // (and whether it worked) does not have to register three. Each also
12740
+ // receives the outcome event (navi_action_abort/error/end) as its
12741
+ // second argument.
12722
12742
  const addSideEffect = (sideEffect) => {
12723
12743
  if (typeof sideEffect === "function") {
12724
- addAbortCallback((reason) => sideEffect({ aborted: true, reason }));
12725
- addErrorCallback((error) => sideEffect({ error }));
12726
- addCompleteCallback((data) => sideEffect({ data }));
12744
+ addAbortCallback((reason, outcomeEvent) =>
12745
+ sideEffect({ aborted: true, reason }, outcomeEvent),
12746
+ );
12747
+ addErrorCallback((error, outcomeEvent) =>
12748
+ sideEffect({ error }, outcomeEvent),
12749
+ );
12750
+ addCompleteCallback((data, outcomeEvent) =>
12751
+ sideEffect({ data }, outcomeEvent),
12752
+ );
12727
12753
  return;
12728
12754
  }
12729
12755
  const { abort, error, complete } = sideEffect;
@@ -12731,56 +12757,6 @@ const useExecuteAction = (
12731
12757
  addErrorCallback(error);
12732
12758
  addCompleteCallback(complete);
12733
12759
  };
12734
- addSideEffect({
12735
- abort: (reason) => {
12736
- const element = elementRef.current;
12737
- if (
12738
- // at this stage the action side effect might have removed the <element> from the DOM
12739
- // (in theory no because action side effect are batched to happen after)
12740
- // but other side effects might do this
12741
- element
12742
- ) {
12743
- dispatchInternalCustomEvent(element, "navi_action_abort", {
12744
- ...sharedActionEventDetail,
12745
- reason,
12746
- });
12747
- }
12748
- },
12749
- error: (error) => {
12750
- if (errorEffect === "show_validation_message") {
12751
- addErrorMessage(error, { requester });
12752
- } else if (errorEffect === "throw") {
12753
- setError(error);
12754
- }
12755
-
12756
- const element = elementRef.current;
12757
- if (
12758
- // at this stage the action side effect might have removed the <element> from the DOM
12759
- // (in theory no because action side effect are batched to happen after)
12760
- // but other side effects might do this
12761
- element
12762
- ) {
12763
- dispatchInternalCustomEvent(element, "navi_action_error", {
12764
- ...sharedActionEventDetail,
12765
- error,
12766
- });
12767
- }
12768
- },
12769
- complete: (data) => {
12770
- const element = elementRef.current;
12771
- if (
12772
- // at this stage the action side effect might have removed the <element> from the DOM
12773
- // (in theory no because action side effect are batched to happen after)
12774
- // but other side effects might do this
12775
- element
12776
- ) {
12777
- dispatchInternalCustomEvent(element, "navi_action_end", {
12778
- ...sharedActionEventDetail,
12779
- data,
12780
- });
12781
- }
12782
- },
12783
- });
12784
12760
 
12785
12761
  const actionStartEventDetail = {
12786
12762
  ...sharedActionEventDetail,
@@ -12792,13 +12768,50 @@ const useExecuteAction = (
12792
12768
  actionStartEventDetail,
12793
12769
  );
12794
12770
 
12771
+ // The outcome is told twice: to the element, when there still is one,
12772
+ // and to the side effects registered at navi_action_start, always. A
12773
+ // run can outlive its control — the run's own answer, written to a
12774
+ // store the parent reads to decide what it draws, unmounts the control
12775
+ // one microtask before the run settles — and the element is then no
12776
+ // place to dispatch to: Preact drops a detached element's listeners.
12777
+ // The side effects are the run's, not the element's, so the event
12778
+ // reaches them either way.
12779
+ const tellOutcome = (outcomeEventName, outcomeEventDetail, trigger) => {
12780
+ const outcomeEvent = createInternalCustomEvent(outcomeEventName, {
12781
+ ...sharedActionEventDetail,
12782
+ ...outcomeEventDetail,
12783
+ });
12784
+ const element = elementRef.current;
12785
+ if (element) {
12786
+ element.dispatchEvent(outcomeEvent);
12787
+ }
12788
+ trigger(outcomeEvent);
12789
+ };
12790
+
12795
12791
  const runAction = () => {
12796
12792
  return action[method]({
12797
12793
  event: actionEvent,
12798
12794
  reason: `"${event.type}" event on ${getElementSignature(event.target)}`,
12799
- onAbort: triggerAbort,
12800
- onError: triggerError,
12801
- onComplete: triggerComplete,
12795
+ onAbort: (reason) => {
12796
+ tellOutcome("navi_action_abort", { reason }, (outcomeEvent) => {
12797
+ triggerAbort(reason, outcomeEvent);
12798
+ });
12799
+ },
12800
+ onError: (error) => {
12801
+ if (errorEffect === "show_validation_message") {
12802
+ addErrorMessage(error, { requester });
12803
+ } else if (errorEffect === "throw") {
12804
+ setError(error);
12805
+ }
12806
+ tellOutcome("navi_action_error", { error }, (outcomeEvent) => {
12807
+ triggerError(error, outcomeEvent);
12808
+ });
12809
+ },
12810
+ onComplete: (data) => {
12811
+ tellOutcome("navi_action_end", { data }, (outcomeEvent) => {
12812
+ triggerComplete(data, outcomeEvent);
12813
+ });
12814
+ },
12802
12815
  });
12803
12816
  };
12804
12817
 
@@ -16508,6 +16521,11 @@ const generateSignalId = () => {
16508
16521
  * as the user moved. `"push"` is for a state whose values ARE places one came
16509
16522
  * from (the photo one is looking at in a gallery). Whatever is said here, one
16510
16523
  * write can say otherwise: `signal.set(value, { history })`.
16524
+ * Either way every write reaches the address, synchronously, and browsers
16525
+ * refuse an address written too often (Safari: 100 writes per 10 s, then a
16526
+ * SecurityError). A value one DRAGS is written at 60 Hz: keep it in the
16527
+ * gesture while the finger is down and write the state once, on release —
16528
+ * an address is not a recording of a gesture.
16511
16529
  * @param {boolean} [options.debug=false] - Enable debug logging for this signal's operations
16512
16530
  * @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.
16513
16531
  *
@@ -26543,11 +26561,7 @@ const setupBrowserIntegrationViaHistory = ({
26543
26561
  [NAV_DEPTH_STATE_KEY]: getNavDepth(),
26544
26562
  },
26545
26563
  });
26546
- if (navigationType === "push") {
26547
- window.history.pushState(effectiveState, null, url);
26548
- } else {
26549
- window.history.replaceState(effectiveState, null, url);
26550
- }
26564
+ writeHistoryEntry(navigationType, effectiveState, url);
26551
26565
  rememberEntryIsOfThisDocument();
26552
26566
  updateDocumentUrl(url);
26553
26567
  updateDocumentState(effectiveState);
@@ -26567,14 +26581,14 @@ const setupBrowserIntegrationViaHistory = ({
26567
26581
  [NAV_DEPTH_STATE_KEY]: getNavDepth(),
26568
26582
  },
26569
26583
  });
26570
- window.history.replaceState(state, null, url);
26584
+ writeHistoryEntry("replace", state, url);
26571
26585
  rememberEntryIsOfThisDocument();
26572
26586
  } else if (redirected) {
26573
26587
  // The entry the browser is on names an address that only sends
26574
26588
  // elsewhere — a cold load on it, or a back into it. Written over where
26575
26589
  // it stands (the entry keeps its place in the stack, hence its state
26576
26590
  // and the depth in it): pressing back must not walk into it again.
26577
- window.history.replaceState(state, null, url);
26591
+ writeHistoryEntry("replace", state, url);
26578
26592
  rememberEntryIsOfThisDocument();
26579
26593
  }
26580
26594
  updateDocumentUrl(url);
@@ -26827,7 +26841,7 @@ const setupBrowserIntegrationViaHistory = ({
26827
26841
  // The entry itself has to lose them too, not just the document state:
26828
26842
  // getDocumentState() reads the entry back, and every state write copies
26829
26843
  // what it finds there onto the next one.
26830
- window.history.replaceState(state, null, url);
26844
+ writeHistoryEntry("replace", state, url);
26831
26845
  }
26832
26846
  handleRoutingTask(url, {
26833
26847
  reason: "routing initialization",
@@ -26849,11 +26863,25 @@ const setupBrowserIntegrationViaHistory = ({
26849
26863
  visitedUrlsSignal,
26850
26864
  };
26851
26865
  };
26866
+ 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.`;
26867
+ const writeHistoryEntry = (navigationType, state, url) => {
26868
+ try {
26869
+ if (navigationType === "push") {
26870
+ window.history.pushState(state, null, url);
26871
+ } else {
26872
+ window.history.replaceState(state, null, url);
26873
+ }
26874
+ } catch (e) {
26875
+ if (e.name !== "SecurityError") {
26876
+ throw e;
26877
+ }
26878
+ throw new Error(
26879
+ `The browser refused to write the address ${url}: ${e.message}. ${ADDRESS_WRITE_ADVICE}`,
26880
+ { cause: e },
26881
+ );
26882
+ }
26883
+ };
26852
26884
 
26853
- // Read before the history is written, in both places that ask: a push or a
26854
- // replace onto the url already displayed. Never a traverse — the browser has
26855
- // already moved window.location.href when a popstate is handled, so the
26856
- // comparison would say "same" about every back and forward.
26857
26885
  const isStateOnlyNavigation = (
26858
26886
  url,
26859
26887
  { navigationType },
@@ -39868,10 +39896,24 @@ const useInteractiveProps = (props, {
39868
39896
  // aborted run, whose promise is awaited to completion (see
39869
39897
  // performRun in actions.js) — which is exactly what "the server is
39870
39898
  // done with it" means, and therefore when the queued request may go.
39871
- e.detail.addSideEffect(outcome => {
39899
+ e.detail.addSideEffect((outcome, outcomeEvent) => {
39872
39900
  uiStateController.actionInFlight = false;
39873
39901
  uiStateController.runningActionSignal.value = null;
39874
39902
  uiStateController.parallelGuard?.release(uiStateController);
39903
+ // The outcome callbacks are told from here, the run's side effect,
39904
+ // and not from the onnavi_action_* handlers below: the run can
39905
+ // outlive this control (its own answer, written to a store the
39906
+ // parent reads, unmounts it one microtask before it settles), and
39907
+ // whoever wears the run — a parent's `loading` following these,
39908
+ // see 12_picker_card_demo.html — must hear it end all the same.
39909
+ // The handlers below keep what needs the element.
39910
+ if (outcome.aborted) {
39911
+ onActionAborted?.(outcomeEvent);
39912
+ } else if ("error" in outcome) {
39913
+ onActionError?.(outcome.error, outcomeEvent);
39914
+ } else {
39915
+ onActionEnd?.(outcome.data, outcomeEvent);
39916
+ }
39875
39917
  const queuedEvent = uiStateController.queuedActionAllowedEvent;
39876
39918
  if (!queuedEvent) {
39877
39919
  syncOptimisticAttribute(false);
@@ -39911,7 +39953,6 @@ const useInteractiveProps = (props, {
39911
39953
  if (resetOnAbort && !superseded) {
39912
39954
  dispatchRequestResetUIState(e.currentTarget, e);
39913
39955
  }
39914
- onActionAborted?.(e);
39915
39956
  },
39916
39957
  onnavi_action_error: e => {
39917
39958
  const {
@@ -39920,7 +39961,6 @@ const useInteractiveProps = (props, {
39920
39961
  if (resetOnError) {
39921
39962
  dispatchRequestResetUIState(e.currentTarget, e);
39922
39963
  }
39923
- onActionError?.(error, e);
39924
39964
  uiStateController.onActionError(e);
39925
39965
  },
39926
39966
  onnavi_action_end: e => {
@@ -39928,7 +39968,6 @@ const useInteractiveProps = (props, {
39928
39968
  data
39929
39969
  } = e.detail;
39930
39970
  debugAction(e, `action end with data: ${JSON.stringify(data)}`);
39931
- onActionEnd?.(data, e);
39932
39971
  controlRootProps.onnavi_action_end?.(e);
39933
39972
  uiStateController.onActionEnd(e);
39934
39973
 
@@ -59316,34 +59355,44 @@ const css$E = /* css */`
59316
59355
  * `showModal()`'d (native focus trap, `Escape`-to-cancel, hardware
59317
59356
  * back-button dismissal, rest-of-document made inert), without one it is
59318
59357
  * shown through the Popover API and the page behind stays live.
59319
- * @param {boolean} [props.dockedOnSmallTouchScreen] - Turns the dialog into a
59320
- * bottom sheet (docked flush to the bottom edge, full width) on a small touch
59321
- * screen, and leaves it alone otherwise. For a dialog meant to be interacted
59322
- * with rather than merely read: on a phone the keyboard owns the bottom of
59323
- * the screen and a centered box ends up both cramped and out of thumb reach,
59324
- * while under a mouse the centered box is already the right shape. Both
59325
- * halves of the name matter (`smallTouchScreenSignal`): touch alone would
59326
- * dock a big touch screen — a tablet, a kiosk panel — a whole screen away
59327
- * from where the finger just tapped, and size alone would dock a narrow
59328
- * desktop window, which is still a mouse. It supplies defaults for
59329
- * `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`, so
59330
- * any of them can still be pinned explicitly — including `expandX={false}`,
59358
+ * @param {boolean|"top"|"bottom"} [props.dockedOnSmallTouchScreen] - Turns
59359
+ * the dialog into a sheet (docked flush to one edge, full width) on a small
59360
+ * touch screen, and leaves it alone otherwise: under a mouse the centered box
59361
+ * is already the right shape, while on a phone a centered box ends up cramped
59362
+ * between the keyboard and the top of the screen. The value says which edge
59363
+ * the sheet rests on. `true` (or `"top"`) rests it on the top edge, out of
59364
+ * the virtual keyboard's way: a field tapped in the sheet raises the keyboard
59365
+ * below it, and the question and the field stay where the finger found them.
59366
+ * `"bottom"` rests it on the bottom edge, where the thumbs are — for a sheet
59367
+ * that is read and tapped (a list, a couple of buttons, a confirmation) and
59368
+ * holds nothing that raises a keyboard; one that does is reflowed into the
59369
+ * strip left above the keyboard at every keystroke. The edge is said here
59370
+ * rather than through `positionArea`, which places both shapes:
59371
+ * `positionArea="bottom"` would also pin the centered box to the bottom of a
59372
+ * desktop window. Both halves of the name matter (`smallTouchScreenSignal`):
59373
+ * touch alone would dock a big touch screen — a tablet, a kiosk panel — a
59374
+ * whole screen away from where the finger just tapped, and size alone would
59375
+ * dock a narrow desktop window, which is still a mouse. It supplies defaults
59376
+ * for `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`,
59377
+ * so any of them can still be pinned explicitly — including `expandX={false}`,
59331
59378
  * which opts the docked dialog out of the full-width stretch and leaves it a
59332
- * floating box at the bottom. It also withdraws `maxWidth` while docked: a
59379
+ * floating box against its edge. It also withdraws `maxWidth` while docked: a
59333
59380
  * sheet is container-wide by definition, and a `maxWidth` is an answer about
59334
59381
  * the *centered* shape, so the two can be stated together (`maxWidth="16rem"
59335
59382
  * dockedOnSmallTouchScreen`) and each applies where it means something.
59336
59383
  * `minWidth` needs no such rule — its floor is below the full width — and
59337
59384
  * `maxHeight`/`minHeight` keep applying, a sheet being content-tall.
59338
- * Ignored entirely when `expandY`
59339
- * (or `expand`) is set: a dialog already filling the height is on the bottom
59340
- * edge docking would bring it to, so docking could only take away the shape
59341
- * the caller asked for. Re-resolves live as the pointer
59342
- * type or the window size changes. A sheet resting on the bottom edge is also
59343
- * pushed back down to close it, held by its header (a `Box` with the `header`
59344
- * prop) and by anything else carrying `data-swipe-grip`. The rest of the sheet
59345
- * is left to what it holds, so a board something is dragged across keeps its
59346
- * own gestures. See `swipe_to_close.js`.
59385
+ * Ignored entirely when `expandY` (or `expand`) is set: a dialog already
59386
+ * filling the height touches both edges, so docking could only take away the
59387
+ * shape the caller asked for. Re-resolves live as the pointer type or the
59388
+ * window size changes. A sheet is also pushed back through the edge it rests
59389
+ * on to close it (down for a bottom sheet, up for a top one), held by its
59390
+ * header (a `Box` with the `header` prop) and by anything else carrying
59391
+ * `data-swipe-grip`. The rest of the sheet is left to what it holds, so a
59392
+ * board something is dragged across keeps its own gestures. See
59393
+ * `swipe_to_close.js`. In dev, a `"bottom"` sheet found holding a field that
59394
+ * raises the keyboard is warned about at open, on any screen — the mistake
59395
+ * itself only shows on a phone.
59347
59396
  * @param {string} [props.positionArea="center"] - Where to dock the dialog
59348
59397
  * within its container (the viewport for `layer="top"`, the positioned
59349
59398
  * ancestor for `layer="local"`) — Dialog is never anchored to a real
@@ -59709,28 +59758,28 @@ const DialogLocal = props => {
59709
59758
  * own to render: a modal's is the native `::backdrop`, and `backdrop={false}`
59710
59759
  * has none at all.
59711
59760
  */
59712
- // What a dialog turns into on a small touch screen. "bottom" is not a taste:
59713
- // it puts the dialog in the zone a phone is actually operated from — where the
59714
- // thumbs rest and where the virtual keyboard comes up — instead of the middle
59715
- // of the screen, which is the farthest point from both.
59761
+ // What a dialog turns into on a small touch screen: a sheet flush against one
59762
+ // edge of the screen. Which edge is the caller's word (the prop's value, see
59763
+ // its JSDoc), the top one unless told "bottom": the cheap mistake must be the
59764
+ // default one — a sheet docked top by mistake is a little farther from the
59765
+ // thumb, a sheet docked bottom by mistake fights the virtual keyboard, which
59766
+ // rises from that very edge, on every keystroke.
59716
59767
  // Only defaults: an explicitly passed prop still wins, so the docked shape can
59717
59768
  // be adjusted one axis at a time instead of being all-or-nothing.
59718
59769
  const DOCKED = {
59719
- positionArea: "bottom",
59720
59770
  marginWithContainer: 0,
59721
59771
  expandX: true,
59722
- // A sheet resting on the bottom edge is dragged with a thumb, and a drag that
59723
- // runs past its own edge must not land on the page behind it: the same
59724
- // reasoning as "bottom" above, applied to the gesture instead of the shape.
59772
+ // A sheet is dragged with a thumb, and a drag that runs past its own edge
59773
+ // must not land on the page behind it.
59725
59774
  scrollCapture: true
59726
59775
  };
59727
59776
 
59728
- // Where a bottom sheet is held to push it back down: the strip a Box declares
59729
- // with `header`, plus anything the application marked as one more. Everything
59730
- // else in the sheet is content the finger came to operate — a board a piece is
59731
- // dragged across, a list, a map — and a press there belongs to it. A sheet with
59732
- // no header and nothing marked is not pushed down at all; it is closed by its
59733
- // own controls, by the backdrop and by Escape.
59777
+ // Where a sheet is held to push it back through its edge: the strip a Box
59778
+ // declares with `header`, plus anything the application marked as one more.
59779
+ // Everything else in the sheet is content the finger came to operate — a board
59780
+ // a piece is dragged across, a list, a map — and a press there belongs to it. A
59781
+ // sheet with no header and nothing marked is not pushed back at all; it is
59782
+ // closed by its own controls, by the backdrop and by Escape.
59734
59783
  const DOCKED_SWIPE_GRIP = "[data-header],[data-swipe-grip]";
59735
59784
  const useDialogProps = props => {
59736
59785
  const backdropProps = {};
@@ -59836,12 +59885,12 @@ const useDialogProps = props => {
59836
59885
  // Only a small touch screen changes anything: on a mouse — and on a touch
59837
59886
  // screen too big to reach the bottom edge of — a dialog already wants to be
59838
59887
  // the centered box it is by default, so there is nothing to resolve.
59839
- // expandY cancels the docking outright: docking exists to bring the dialog
59840
- // down to the edge the thumb is on, and a dialog already filling the height
59841
- // is on that edge — all docking could still do is take away the shape the
59842
- // caller asked for (and arm a swipe-down on something that never rose).
59888
+ // expandY cancels the docking outright: a dialog already filling the height
59889
+ // touches both edges — all docking could still do is take away the shape
59890
+ // the caller asked for (and arm a swipe on something that never rose).
59843
59891
  const isDocked = dockedOnSmallTouchScreen && smallTouchScreenSignal.value && !expandY;
59844
- const positionArea = positionAreaProp ?? (isDocked ? DOCKED.positionArea : "center");
59892
+ const dockedEdge = dockedOnSmallTouchScreen === "bottom" ? "bottom" : "top";
59893
+ const positionArea = positionAreaProp ?? (isDocked ? dockedEdge : "center");
59845
59894
  const marginWithContainer = marginWithContainerProp ?? (isDocked ? DOCKED.marginWithContainer :
59846
59895
  // A share of whatever holds the dialog: the app's own screen for a
59847
59896
  // top-layer one — where appw is exactly "3% of the container", the
@@ -59903,12 +59952,12 @@ const useDialogProps = props => {
59903
59952
  y: "center",
59904
59953
  x: "center"
59905
59954
  };
59906
- // Pushing the sheet back down closes it — a bottom sheet is reached with a
59907
- // thumb, and the thumb is already on the edge it would push. Only a sheet
59908
- // actually resting on the bottom edge: anywhere else the gesture would send
59909
- // the dialog somewhere it never came from.
59910
- const swipeToCloseDown = isDocked && parsedPositionArea.y === "bottom";
59911
- const onSwipePointerDown = swipeToCloseDown ? createSwipeToClose("bottom", {
59955
+ // Pushing the sheet back through the edge it rests on closes it — the way
59956
+ // it came in, and the way the thumb on that edge already pushes. Only a sheet
59957
+ // actually resting on an edge: anywhere else the gesture would send the
59958
+ // dialog somewhere it never came from.
59959
+ const swipeToCloseSide = isDocked && (parsedPositionArea.y === "top" || parsedPositionArea.y === "bottom") ? parsedPositionArea.y : null;
59960
+ const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose(swipeToCloseSide, {
59912
59961
  grip: DOCKED_SWIPE_GRIP
59913
59962
  }) : null;
59914
59963
  // A corner sitting exactly on the container's own corner must not be
@@ -60540,12 +60589,12 @@ const useDialogProps = props => {
60540
60589
  "data-flush-right": flushEdges.right ? "" : undefined,
60541
60590
  "data-flush-bottom": flushEdges.bottom ? "" : undefined,
60542
60591
  "data-flush-left": flushEdges.left ? "" : undefined,
60543
- "data-swipe-to-close": swipeToCloseDown ? "" : undefined,
60592
+ "data-swipe-to-close": swipeToCloseSide ?? undefined,
60544
60593
  // The axis the sheet travels on when it is pushed back, said to the shared
60545
60594
  // gesture layer: it is what a box travelling inside the sheet reads to know
60546
60595
  // this axis is already walked (see @jsenv/dom's drag_to_travel).
60547
- "data-drag-travel": swipeToCloseDown ? SWIPE_AXIS_BY_SIDE.bottom : undefined,
60548
- "data-travel-by-drag": swipeToCloseDown ? SWIPE_AXIS_BY_SIDE.bottom : undefined,
60596
+ "data-drag-travel": swipeToCloseSide ? SWIPE_AXIS_BY_SIDE[swipeToCloseSide] : undefined,
60597
+ "data-travel-by-drag": swipeToCloseSide ? SWIPE_AXIS_BY_SIDE[swipeToCloseSide] : undefined,
60549
60598
  "onPointerDown": e => {
60550
60599
  rest.onPointerDown?.(e);
60551
60600
  onSwipePointerDown?.(e);
@@ -62339,8 +62388,8 @@ const css$C = /* css */`@layer navi {
62339
62388
  * @param {string} [props.positionAreaFixed] - **Popover-only**, same guard.
62340
62389
  * @param {string} [props.positionAreaWhenAnchorIsInvalid] - **Popover-only**,
62341
62390
  * same guard.
62342
- * @param {boolean} [props.dockedOnSmallTouchScreen] - **Dialog-only** (a
62343
- * popover is never a bottom sheet), destructured out for the same reason
62391
+ * @param {boolean|"top"|"bottom"} [props.dockedOnSmallTouchScreen] -
62392
+ * **Dialog-only** (a popover is never a sheet), destructured out for the same reason
62344
62393
  * turned around: it must not land on the popover element as a stray DOM
62345
62394
  * attribute when the screen-size resolution picks `mode="popover"`. Where
62346
62395
  * that resolution lands is what decides whether it applies at all, so a
@@ -62368,7 +62417,7 @@ const css$C = /* css */`@layer navi {
62368
62417
  * honoured in either mode — a wall-less `Dialog` is shown through the
62369
62418
  * Popover API rather than `showModal()` — so which mode the screen-size
62370
62419
  * resolution picks says nothing about whether the page behind stays live.
62371
- * It is also what makes a sheet docked to the bottom of a phone's screen
62420
+ * It is also what makes a sheet docked to an edge of a phone's screen
62372
62421
  * (`dockedOnSmallTouchScreen`) non-modal.
62373
62422
  * @param {"auto"|"discrete"|"invisible"} [props.backdropVariant] - Forwarded
62374
62423
  * as-is to whichever component renders (both understand it identically):
@@ -74973,7 +75022,7 @@ const css$n = /* css */`.navi_split_button {
74973
75022
  * dialogExpand?: boolean,
74974
75023
  * dialogExpandX?: boolean,
74975
75024
  * dialogExpandY?: boolean,
74976
- * dockedOnSmallTouchScreen?: boolean,
75025
+ * dockedOnSmallTouchScreen?: boolean | "top" | "bottom",
74977
75026
  * marginWithContainer?: number | string,
74978
75027
  * backdrop?: boolean,
74979
75028
  * backdropVariant?: "auto" | "discrete" | "invisible",
@@ -84401,6 +84450,9 @@ const createSlot = (SlotRenderer = Box) => {
84401
84450
  slotPropsSignal.value = props;
84402
84451
  useLayoutEffect(() => {
84403
84452
  return () => {
84453
+ // Within one diff preact mounts the newcomer before unmounting the
84454
+ // leaver, so the leaver may no longer be the filler by the time it
84455
+ // is cleaned up.
84404
84456
  if (filler !== fillerRef) {
84405
84457
  return;
84406
84458
  }