@jsenv/navi 0.29.324 → 0.29.325
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 +174 -116
- package/dist/dev/jsenv_navi.js.map +8 -8
- package/dist/jsenv_navi.js +141 -116
- package/dist/jsenv_navi.js.map +8 -8
- 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";
|
|
@@ -13207,6 +13207,12 @@ const useExecuteAction = (
|
|
|
13207
13207
|
// whose command sent — is stored as the callout display target, so the
|
|
13208
13208
|
// message appears on whoever asked rather than on the whole control.
|
|
13209
13209
|
const element = elementRef.current;
|
|
13210
|
+
if (!element) {
|
|
13211
|
+
// The control left the page while its run was out (its own answer can
|
|
13212
|
+
// unmount it, see the outcome events below): there is no box to hang
|
|
13213
|
+
// the refusal on. onActionError is still told, by the run's side effect.
|
|
13214
|
+
return;
|
|
13215
|
+
}
|
|
13210
13216
|
let target = requester;
|
|
13211
13217
|
// A requester that is no longer on the page is not a place to show
|
|
13212
13218
|
// anything: the clear cross leaves with the value it optimistically
|
|
@@ -13310,12 +13316,20 @@ const useExecuteAction = (
|
|
|
13310
13316
|
// Either three callbacks, one per outcome, or a single one for "however
|
|
13311
13317
|
// this ends" — it receives `{ aborted, reason }`, `{ error }` or
|
|
13312
13318
|
// `{ data }`, so a caller that only needs to know the action settled
|
|
13313
|
-
// (and whether it worked) does not have to register three.
|
|
13319
|
+
// (and whether it worked) does not have to register three. Each also
|
|
13320
|
+
// receives the outcome event (navi_action_abort/error/end) as its
|
|
13321
|
+
// second argument.
|
|
13314
13322
|
const addSideEffect = (sideEffect) => {
|
|
13315
13323
|
if (typeof sideEffect === "function") {
|
|
13316
|
-
addAbortCallback((reason) =>
|
|
13317
|
-
|
|
13318
|
-
|
|
13324
|
+
addAbortCallback((reason, outcomeEvent) =>
|
|
13325
|
+
sideEffect({ aborted: true, reason }, outcomeEvent),
|
|
13326
|
+
);
|
|
13327
|
+
addErrorCallback((error, outcomeEvent) =>
|
|
13328
|
+
sideEffect({ error }, outcomeEvent),
|
|
13329
|
+
);
|
|
13330
|
+
addCompleteCallback((data, outcomeEvent) =>
|
|
13331
|
+
sideEffect({ data }, outcomeEvent),
|
|
13332
|
+
);
|
|
13319
13333
|
return;
|
|
13320
13334
|
}
|
|
13321
13335
|
const { abort, error, complete } = sideEffect;
|
|
@@ -13323,56 +13337,6 @@ const useExecuteAction = (
|
|
|
13323
13337
|
addErrorCallback(error);
|
|
13324
13338
|
addCompleteCallback(complete);
|
|
13325
13339
|
};
|
|
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
13340
|
|
|
13377
13341
|
const actionStartEventDetail = {
|
|
13378
13342
|
...sharedActionEventDetail,
|
|
@@ -13384,13 +13348,50 @@ const useExecuteAction = (
|
|
|
13384
13348
|
actionStartEventDetail,
|
|
13385
13349
|
);
|
|
13386
13350
|
|
|
13351
|
+
// The outcome is told twice: to the element, when there still is one,
|
|
13352
|
+
// and to the side effects registered at navi_action_start, always. A
|
|
13353
|
+
// run can outlive its control — the run's own answer, written to a
|
|
13354
|
+
// store the parent reads to decide what it draws, unmounts the control
|
|
13355
|
+
// one microtask before the run settles — and the element is then no
|
|
13356
|
+
// place to dispatch to: Preact drops a detached element's listeners.
|
|
13357
|
+
// The side effects are the run's, not the element's, so the event
|
|
13358
|
+
// reaches them either way.
|
|
13359
|
+
const tellOutcome = (outcomeEventName, outcomeEventDetail, trigger) => {
|
|
13360
|
+
const outcomeEvent = createInternalCustomEvent(outcomeEventName, {
|
|
13361
|
+
...sharedActionEventDetail,
|
|
13362
|
+
...outcomeEventDetail,
|
|
13363
|
+
});
|
|
13364
|
+
const element = elementRef.current;
|
|
13365
|
+
if (element) {
|
|
13366
|
+
element.dispatchEvent(outcomeEvent);
|
|
13367
|
+
}
|
|
13368
|
+
trigger(outcomeEvent);
|
|
13369
|
+
};
|
|
13370
|
+
|
|
13387
13371
|
const runAction = () => {
|
|
13388
13372
|
return action[method]({
|
|
13389
13373
|
event: actionEvent,
|
|
13390
13374
|
reason: `"${event.type}" event on ${getElementSignature(event.target)}`,
|
|
13391
|
-
onAbort:
|
|
13392
|
-
|
|
13393
|
-
|
|
13375
|
+
onAbort: (reason) => {
|
|
13376
|
+
tellOutcome("navi_action_abort", { reason }, (outcomeEvent) => {
|
|
13377
|
+
triggerAbort(reason, outcomeEvent);
|
|
13378
|
+
});
|
|
13379
|
+
},
|
|
13380
|
+
onError: (error) => {
|
|
13381
|
+
if (errorEffect === "show_validation_message") {
|
|
13382
|
+
addErrorMessage(error, { requester });
|
|
13383
|
+
} else if (errorEffect === "throw") {
|
|
13384
|
+
setError(error);
|
|
13385
|
+
}
|
|
13386
|
+
tellOutcome("navi_action_error", { error }, (outcomeEvent) => {
|
|
13387
|
+
triggerError(error, outcomeEvent);
|
|
13388
|
+
});
|
|
13389
|
+
},
|
|
13390
|
+
onComplete: (data) => {
|
|
13391
|
+
tellOutcome("navi_action_end", { data }, (outcomeEvent) => {
|
|
13392
|
+
triggerComplete(data, outcomeEvent);
|
|
13393
|
+
});
|
|
13394
|
+
},
|
|
13394
13395
|
});
|
|
13395
13396
|
};
|
|
13396
13397
|
|
|
@@ -40910,10 +40911,24 @@ const useInteractiveProps = (props, {
|
|
|
40910
40911
|
// aborted run, whose promise is awaited to completion (see
|
|
40911
40912
|
// performRun in actions.js) — which is exactly what "the server is
|
|
40912
40913
|
// done with it" means, and therefore when the queued request may go.
|
|
40913
|
-
e.detail.addSideEffect(outcome => {
|
|
40914
|
+
e.detail.addSideEffect((outcome, outcomeEvent) => {
|
|
40914
40915
|
uiStateController.actionInFlight = false;
|
|
40915
40916
|
uiStateController.runningActionSignal.value = null;
|
|
40916
40917
|
uiStateController.parallelGuard?.release(uiStateController);
|
|
40918
|
+
// The outcome callbacks are told from here, the run's side effect,
|
|
40919
|
+
// and not from the onnavi_action_* handlers below: the run can
|
|
40920
|
+
// outlive this control (its own answer, written to a store the
|
|
40921
|
+
// parent reads, unmounts it one microtask before it settles), and
|
|
40922
|
+
// whoever wears the run — a parent's `loading` following these,
|
|
40923
|
+
// see 12_picker_card_demo.html — must hear it end all the same.
|
|
40924
|
+
// The handlers below keep what needs the element.
|
|
40925
|
+
if (outcome.aborted) {
|
|
40926
|
+
onActionAborted?.(outcomeEvent);
|
|
40927
|
+
} else if ("error" in outcome) {
|
|
40928
|
+
onActionError?.(outcome.error, outcomeEvent);
|
|
40929
|
+
} else {
|
|
40930
|
+
onActionEnd?.(outcome.data, outcomeEvent);
|
|
40931
|
+
}
|
|
40917
40932
|
const queuedEvent = uiStateController.queuedActionAllowedEvent;
|
|
40918
40933
|
if (!queuedEvent) {
|
|
40919
40934
|
syncOptimisticAttribute(false);
|
|
@@ -40953,7 +40968,6 @@ const useInteractiveProps = (props, {
|
|
|
40953
40968
|
if (resetOnAbort && !superseded) {
|
|
40954
40969
|
dispatchRequestResetUIState(e.currentTarget, e);
|
|
40955
40970
|
}
|
|
40956
|
-
onActionAborted?.(e);
|
|
40957
40971
|
},
|
|
40958
40972
|
onnavi_action_error: e => {
|
|
40959
40973
|
const {
|
|
@@ -40963,7 +40977,6 @@ const useInteractiveProps = (props, {
|
|
|
40963
40977
|
if (resetOnError) {
|
|
40964
40978
|
dispatchRequestResetUIState(e.currentTarget, e);
|
|
40965
40979
|
}
|
|
40966
|
-
onActionError?.(error, e);
|
|
40967
40980
|
uiStateController.onActionError(e);
|
|
40968
40981
|
},
|
|
40969
40982
|
onnavi_action_end: e => {
|
|
@@ -40971,7 +40984,6 @@ const useInteractiveProps = (props, {
|
|
|
40971
40984
|
data
|
|
40972
40985
|
} = e.detail;
|
|
40973
40986
|
debugAction(e, `action end with data: ${JSON.stringify(data)}`);
|
|
40974
|
-
onActionEnd?.(data, e);
|
|
40975
40987
|
controlRootProps.onnavi_action_end?.(e);
|
|
40976
40988
|
uiStateController.onActionEnd(e);
|
|
40977
40989
|
|
|
@@ -60451,34 +60463,44 @@ const css$E = /* css */`
|
|
|
60451
60463
|
* `showModal()`'d (native focus trap, `Escape`-to-cancel, hardware
|
|
60452
60464
|
* back-button dismissal, rest-of-document made inert), without one it is
|
|
60453
60465
|
* 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
|
-
*
|
|
60466
|
+
* @param {boolean|"top"|"bottom"} [props.dockedOnSmallTouchScreen] - Turns
|
|
60467
|
+
* the dialog into a sheet (docked flush to one edge, full width) on a small
|
|
60468
|
+
* touch screen, and leaves it alone otherwise: under a mouse the centered box
|
|
60469
|
+
* is already the right shape, while on a phone a centered box ends up cramped
|
|
60470
|
+
* between the keyboard and the top of the screen. The value says which edge
|
|
60471
|
+
* the sheet rests on. `true` (or `"top"`) rests it on the top edge, out of
|
|
60472
|
+
* the virtual keyboard's way: a field tapped in the sheet raises the keyboard
|
|
60473
|
+
* below it, and the question and the field stay where the finger found them.
|
|
60474
|
+
* `"bottom"` rests it on the bottom edge, where the thumbs are — for a sheet
|
|
60475
|
+
* that is read and tapped (a list, a couple of buttons, a confirmation) and
|
|
60476
|
+
* holds nothing that raises a keyboard; one that does is reflowed into the
|
|
60477
|
+
* strip left above the keyboard at every keystroke. The edge is said here
|
|
60478
|
+
* rather than through `positionArea`, which places both shapes:
|
|
60479
|
+
* `positionArea="bottom"` would also pin the centered box to the bottom of a
|
|
60480
|
+
* desktop window. Both halves of the name matter (`smallTouchScreenSignal`):
|
|
60481
|
+
* touch alone would dock a big touch screen — a tablet, a kiosk panel — a
|
|
60482
|
+
* whole screen away from where the finger just tapped, and size alone would
|
|
60483
|
+
* dock a narrow desktop window, which is still a mouse. It supplies defaults
|
|
60484
|
+
* for `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`,
|
|
60485
|
+
* so any of them can still be pinned explicitly — including `expandX={false}`,
|
|
60466
60486
|
* which opts the docked dialog out of the full-width stretch and leaves it a
|
|
60467
|
-
* floating box
|
|
60487
|
+
* floating box against its edge. It also withdraws `maxWidth` while docked: a
|
|
60468
60488
|
* sheet is container-wide by definition, and a `maxWidth` is an answer about
|
|
60469
60489
|
* the *centered* shape, so the two can be stated together (`maxWidth="16rem"
|
|
60470
60490
|
* dockedOnSmallTouchScreen`) and each applies where it means something.
|
|
60471
60491
|
* `minWidth` needs no such rule — its floor is below the full width — and
|
|
60472
60492
|
* `maxHeight`/`minHeight` keep applying, a sheet being content-tall.
|
|
60473
|
-
* Ignored entirely when `expandY`
|
|
60474
|
-
*
|
|
60475
|
-
*
|
|
60476
|
-
*
|
|
60477
|
-
*
|
|
60478
|
-
*
|
|
60479
|
-
*
|
|
60480
|
-
*
|
|
60481
|
-
*
|
|
60493
|
+
* Ignored entirely when `expandY` (or `expand`) is set: a dialog already
|
|
60494
|
+
* filling the height touches both edges, so docking could only take away the
|
|
60495
|
+
* shape the caller asked for. Re-resolves live as the pointer type or the
|
|
60496
|
+
* window size changes. A sheet is also pushed back through the edge it rests
|
|
60497
|
+
* on to close it (down for a bottom sheet, up for a top one), held by its
|
|
60498
|
+
* header (a `Box` with the `header` prop) and by anything else carrying
|
|
60499
|
+
* `data-swipe-grip`. The rest of the sheet is left to what it holds, so a
|
|
60500
|
+
* board something is dragged across keeps its own gestures. See
|
|
60501
|
+
* `swipe_to_close.js`. In dev, a `"bottom"` sheet found holding a field that
|
|
60502
|
+
* raises the keyboard is warned about at open, on any screen — the mistake
|
|
60503
|
+
* itself only shows on a phone.
|
|
60482
60504
|
* @param {string} [props.positionArea="center"] - Where to dock the dialog
|
|
60483
60505
|
* within its container (the viewport for `layer="top"`, the positioned
|
|
60484
60506
|
* ancestor for `layer="local"`) — Dialog is never anchored to a real
|
|
@@ -60846,28 +60868,28 @@ const DialogLocal = props => {
|
|
|
60846
60868
|
* own to render: a modal's is the native `::backdrop`, and `backdrop={false}`
|
|
60847
60869
|
* has none at all.
|
|
60848
60870
|
*/
|
|
60849
|
-
// What a dialog turns into on a small touch screen
|
|
60850
|
-
//
|
|
60851
|
-
//
|
|
60852
|
-
//
|
|
60871
|
+
// What a dialog turns into on a small touch screen: a sheet flush against one
|
|
60872
|
+
// edge of the screen. Which edge is the caller's word (the prop's value, see
|
|
60873
|
+
// its JSDoc), the top one unless told "bottom": the cheap mistake must be the
|
|
60874
|
+
// default one — a sheet docked top by mistake is a little farther from the
|
|
60875
|
+
// thumb, a sheet docked bottom by mistake fights the virtual keyboard, which
|
|
60876
|
+
// rises from that very edge, on every keystroke.
|
|
60853
60877
|
// Only defaults: an explicitly passed prop still wins, so the docked shape can
|
|
60854
60878
|
// be adjusted one axis at a time instead of being all-or-nothing.
|
|
60855
60879
|
const DOCKED = {
|
|
60856
|
-
positionArea: "bottom",
|
|
60857
60880
|
marginWithContainer: 0,
|
|
60858
60881
|
expandX: true,
|
|
60859
|
-
// A sheet
|
|
60860
|
-
//
|
|
60861
|
-
// reasoning as "bottom" above, applied to the gesture instead of the shape.
|
|
60882
|
+
// A sheet is dragged with a thumb, and a drag that runs past its own edge
|
|
60883
|
+
// must not land on the page behind it.
|
|
60862
60884
|
scrollCapture: true
|
|
60863
60885
|
};
|
|
60864
60886
|
|
|
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.
|
|
60887
|
+
// Where a sheet is held to push it back through its edge: the strip a Box
|
|
60888
|
+
// declares with `header`, plus anything the application marked as one more.
|
|
60889
|
+
// Everything else in the sheet is content the finger came to operate — a board
|
|
60890
|
+
// a piece is dragged across, a list, a map — and a press there belongs to it. A
|
|
60891
|
+
// sheet with no header and nothing marked is not pushed back at all; it is
|
|
60892
|
+
// closed by its own controls, by the backdrop and by Escape.
|
|
60871
60893
|
const DOCKED_SWIPE_GRIP = "[data-header],[data-swipe-grip]";
|
|
60872
60894
|
const useDialogProps = props => {
|
|
60873
60895
|
const backdropProps = {};
|
|
@@ -60973,12 +60995,15 @@ const useDialogProps = props => {
|
|
|
60973
60995
|
// Only a small touch screen changes anything: on a mouse — and on a touch
|
|
60974
60996
|
// screen too big to reach the bottom edge of — a dialog already wants to be
|
|
60975
60997
|
// 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).
|
|
60998
|
+
// expandY cancels the docking outright: a dialog already filling the height
|
|
60999
|
+
// touches both edges — all docking could still do is take away the shape
|
|
61000
|
+
// the caller asked for (and arm a swipe on something that never rose).
|
|
60980
61001
|
const isDocked = dockedOnSmallTouchScreen && smallTouchScreenSignal.value && !expandY;
|
|
60981
|
-
const
|
|
61002
|
+
const dockedEdge = dockedOnSmallTouchScreen === "bottom" ? "bottom" : "top";
|
|
61003
|
+
const positionArea = positionAreaProp ?? (isDocked ? dockedEdge : "center");
|
|
61004
|
+
// Read off the shape a phone would give, not the one this screen gives: the
|
|
61005
|
+
// author of a bottom sheet tests on a desktop, where the mistake is silent.
|
|
61006
|
+
const sheetRestsOnBottom = Boolean(dockedOnSmallTouchScreen) && !expandY && restsOnBottom(positionAreaProp ?? dockedEdge);
|
|
60982
61007
|
const marginWithContainer = marginWithContainerProp ?? (isDocked ? DOCKED.marginWithContainer :
|
|
60983
61008
|
// A share of whatever holds the dialog: the app's own screen for a
|
|
60984
61009
|
// top-layer one — where appw is exactly "3% of the container", the
|
|
@@ -61040,12 +61065,12 @@ const useDialogProps = props => {
|
|
|
61040
61065
|
y: "center",
|
|
61041
61066
|
x: "center"
|
|
61042
61067
|
};
|
|
61043
|
-
// Pushing the sheet back
|
|
61044
|
-
//
|
|
61045
|
-
// actually resting on
|
|
61046
|
-
//
|
|
61047
|
-
const
|
|
61048
|
-
const onSwipePointerDown =
|
|
61068
|
+
// Pushing the sheet back through the edge it rests on closes it — the way
|
|
61069
|
+
// it came in, and the way the thumb on that edge already pushes. Only a sheet
|
|
61070
|
+
// actually resting on an edge: anywhere else the gesture would send the
|
|
61071
|
+
// dialog somewhere it never came from.
|
|
61072
|
+
const swipeToCloseSide = isDocked && (parsedPositionArea.y === "top" || parsedPositionArea.y === "bottom") ? parsedPositionArea.y : null;
|
|
61073
|
+
const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose(swipeToCloseSide, {
|
|
61049
61074
|
grip: DOCKED_SWIPE_GRIP
|
|
61050
61075
|
}) : null;
|
|
61051
61076
|
// A corner sitting exactly on the container's own corner must not be
|
|
@@ -61173,6 +61198,9 @@ const useDialogProps = props => {
|
|
|
61173
61198
|
}
|
|
61174
61199
|
{
|
|
61175
61200
|
warnAboutUnreachableOutsideRegions(dialogEl);
|
|
61201
|
+
if (sheetRestsOnBottom) {
|
|
61202
|
+
warnBottomSheetHoldingKeyboardControl(dialogEl);
|
|
61203
|
+
}
|
|
61176
61204
|
}
|
|
61177
61205
|
|
|
61178
61206
|
// Set by useOpenControllerByProps for the very first open triggered by
|
|
@@ -61721,12 +61749,12 @@ const useDialogProps = props => {
|
|
|
61721
61749
|
"data-flush-right": flushEdges.right ? "" : undefined,
|
|
61722
61750
|
"data-flush-bottom": flushEdges.bottom ? "" : undefined,
|
|
61723
61751
|
"data-flush-left": flushEdges.left ? "" : undefined,
|
|
61724
|
-
"data-swipe-to-close":
|
|
61752
|
+
"data-swipe-to-close": swipeToCloseSide ?? undefined,
|
|
61725
61753
|
// The axis the sheet travels on when it is pushed back, said to the shared
|
|
61726
61754
|
// gesture layer: it is what a box travelling inside the sheet reads to know
|
|
61727
61755
|
// this axis is already walked (see @jsenv/dom's drag_to_travel).
|
|
61728
|
-
"data-drag-travel":
|
|
61729
|
-
"data-travel-by-drag":
|
|
61756
|
+
"data-drag-travel": swipeToCloseSide ? SWIPE_AXIS_BY_SIDE[swipeToCloseSide] : undefined,
|
|
61757
|
+
"data-travel-by-drag": swipeToCloseSide ? SWIPE_AXIS_BY_SIDE[swipeToCloseSide] : undefined,
|
|
61730
61758
|
"onPointerDown": e => {
|
|
61731
61759
|
rest.onPointerDown?.(e);
|
|
61732
61760
|
onSwipePointerDown?.(e);
|
|
@@ -61798,6 +61826,33 @@ const useDialogProps = props => {
|
|
|
61798
61826
|
}
|
|
61799
61827
|
return [!isTopLayer && backdrop ? backdropProps : null, contentProps];
|
|
61800
61828
|
};
|
|
61829
|
+
const restsOnBottom = positionArea => {
|
|
61830
|
+
const parsed = parsePositionArea(positionArea);
|
|
61831
|
+
return Boolean(parsed) && parsed.y === "bottom";
|
|
61832
|
+
};
|
|
61833
|
+
|
|
61834
|
+
// What raises a virtual keyboard when tapped: a text-like input, a textarea,
|
|
61835
|
+
// anything editable. Not a `select` — it opens a menu, which takes no screen
|
|
61836
|
+
// from the sheet — and not a read-only field.
|
|
61837
|
+
const KEYBOARD_RAISING_CONTROL_SELECTOR = `
|
|
61838
|
+
input:not([type]):not([readonly]),
|
|
61839
|
+
input[type="text"]:not([readonly]),
|
|
61840
|
+
input[type="search"]:not([readonly]),
|
|
61841
|
+
input[type="email"]:not([readonly]),
|
|
61842
|
+
input[type="url"]:not([readonly]),
|
|
61843
|
+
input[type="tel"]:not([readonly]),
|
|
61844
|
+
input[type="password"]:not([readonly]),
|
|
61845
|
+
input[type="number"]:not([readonly]),
|
|
61846
|
+
textarea:not([readonly]),
|
|
61847
|
+
[contenteditable]:not([contenteditable="false"])
|
|
61848
|
+
`;
|
|
61849
|
+
const warnBottomSheetHoldingKeyboardControl = dialogEl => {
|
|
61850
|
+
const controlEl = dialogEl.querySelector(KEYBOARD_RAISING_CONTROL_SELECTOR);
|
|
61851
|
+
if (!controlEl) {
|
|
61852
|
+
return;
|
|
61853
|
+
}
|
|
61854
|
+
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);
|
|
61855
|
+
};
|
|
61801
61856
|
const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
|
|
61802
61857
|
|
|
61803
61858
|
// Lets consumers pass animationDuration="0.5s" as a regular prop; Box maps
|
|
@@ -63529,8 +63584,8 @@ const css$C = /* css */`@layer navi {
|
|
|
63529
63584
|
* @param {string} [props.positionAreaFixed] - **Popover-only**, same guard.
|
|
63530
63585
|
* @param {string} [props.positionAreaWhenAnchorIsInvalid] - **Popover-only**,
|
|
63531
63586
|
* same guard.
|
|
63532
|
-
* @param {boolean} [props.dockedOnSmallTouchScreen] -
|
|
63533
|
-
* popover is never a
|
|
63587
|
+
* @param {boolean|"top"|"bottom"} [props.dockedOnSmallTouchScreen] -
|
|
63588
|
+
* **Dialog-only** (a popover is never a sheet), destructured out for the same reason
|
|
63534
63589
|
* turned around: it must not land on the popover element as a stray DOM
|
|
63535
63590
|
* attribute when the screen-size resolution picks `mode="popover"`. Where
|
|
63536
63591
|
* that resolution lands is what decides whether it applies at all, so a
|
|
@@ -63558,7 +63613,7 @@ const css$C = /* css */`@layer navi {
|
|
|
63558
63613
|
* honoured in either mode — a wall-less `Dialog` is shown through the
|
|
63559
63614
|
* Popover API rather than `showModal()` — so which mode the screen-size
|
|
63560
63615
|
* resolution picks says nothing about whether the page behind stays live.
|
|
63561
|
-
* It is also what makes a sheet docked to
|
|
63616
|
+
* It is also what makes a sheet docked to an edge of a phone's screen
|
|
63562
63617
|
* (`dockedOnSmallTouchScreen`) non-modal.
|
|
63563
63618
|
* @param {"auto"|"discrete"|"invisible"} [props.backdropVariant] - Forwarded
|
|
63564
63619
|
* as-is to whichever component renders (both understand it identically):
|
|
@@ -76813,7 +76868,7 @@ const css$n = /* css */`.navi_split_button {
|
|
|
76813
76868
|
* dialogExpand?: boolean,
|
|
76814
76869
|
* dialogExpandX?: boolean,
|
|
76815
76870
|
* dialogExpandY?: boolean,
|
|
76816
|
-
* dockedOnSmallTouchScreen?: boolean,
|
|
76871
|
+
* dockedOnSmallTouchScreen?: boolean | "top" | "bottom",
|
|
76817
76872
|
* marginWithContainer?: number | string,
|
|
76818
76873
|
* backdrop?: boolean,
|
|
76819
76874
|
* backdropVariant?: "auto" | "discrete" | "invisible",
|
|
@@ -86249,6 +86304,9 @@ const createSlot = (SlotRenderer = Box) => {
|
|
|
86249
86304
|
slotPropsSignal.value = props;
|
|
86250
86305
|
useLayoutEffect(() => {
|
|
86251
86306
|
return () => {
|
|
86307
|
+
// Within one diff preact mounts the newcomer before unmounting the
|
|
86308
|
+
// leaver, so the leaver may no longer be the filler by the time it
|
|
86309
|
+
// is cleaned up.
|
|
86252
86310
|
if (filler !== fillerRef) {
|
|
86253
86311
|
return;
|
|
86254
86312
|
}
|