@jsenv/navi 0.29.89 → 0.29.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/jsenv_navi.js +964 -84
- package/dist/jsenv_navi.js.map +92 -19
- package/docs/AI_INSTRUCTIONS.md +19 -0
- package/docs/actions.md +10 -0
- package/docs/control_object.md +13 -1
- package/docs/control_value.md +23 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
6
6
|
export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
|
|
7
|
-
import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, clickIsSuppressed, isTouchDrivenEvent, scrollIntoViewScoped, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, findBefore, findAfter, initFocusGroup, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement
|
|
7
|
+
import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, clickIsSuppressed, isTouchDrivenEvent, scrollIntoViewScoped, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, findBefore, findAfter, initFocusGroup, stringifyStyle as stringifyStyle$1, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
8
8
|
export { clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
9
9
|
import { signal, computed, effect, batch, untracked, useSignal } from "@preact/signals";
|
|
10
10
|
import { createContext, isValidElement, h, Fragment, render, toChildArray, options, cloneElement } from "preact";
|
|
@@ -38,7 +38,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
38
38
|
* any of these, and a number is the last resort, not the first tool.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
const css$
|
|
41
|
+
const css$13 = /* css */`
|
|
42
42
|
@layer navi {
|
|
43
43
|
:root {
|
|
44
44
|
/* A control that overlaps its neighbours (the members of a Group share
|
|
@@ -92,7 +92,7 @@ const css$12 = /* css */`
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
`;
|
|
95
|
-
import.meta.css = [css$
|
|
95
|
+
import.meta.css = [css$13, "@jsenv/navi/src/navi_z_indexes.js"];
|
|
96
96
|
|
|
97
97
|
const addIntoArray = (array, ...valuesToAdd) => {
|
|
98
98
|
if (valuesToAdd.length === 1) {
|
|
@@ -361,7 +361,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
361
361
|
* the very first render and the browser does everything on its own.
|
|
362
362
|
*/
|
|
363
363
|
const URL_TARGET_ATTRIBUTE = "data-url-target";
|
|
364
|
-
const css$
|
|
364
|
+
const css$12 = /* css */`
|
|
365
365
|
@layer navi {
|
|
366
366
|
[${URL_TARGET_ATTRIBUTE}] {
|
|
367
367
|
animation: navi_url_target var(--navi-url-target-duration, 2000ms)
|
|
@@ -379,7 +379,7 @@ const css$11 = /* css */`
|
|
|
379
379
|
}
|
|
380
380
|
}
|
|
381
381
|
`;
|
|
382
|
-
import.meta.css = [css$
|
|
382
|
+
import.meta.css = [css$12, "@jsenv/navi/src/nav/url_target/url_target.js"];
|
|
383
383
|
let urlTargetOptions = {
|
|
384
384
|
block: "center",
|
|
385
385
|
behavior: "smooth",
|
|
@@ -6403,7 +6403,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
6403
6403
|
* - Arrow automatically shows when pointing at a valid anchor element
|
|
6404
6404
|
* - Centers in viewport when no anchor element provided or anchor is too big
|
|
6405
6405
|
*/
|
|
6406
|
-
const css$
|
|
6406
|
+
const css$11 = /* css */`
|
|
6407
6407
|
@layer navi {
|
|
6408
6408
|
.navi_callout {
|
|
6409
6409
|
/* A callout is parented to what it explains, so it inherits from it — and
|
|
@@ -6646,7 +6646,7 @@ const openCallout = (message, {
|
|
|
6646
6646
|
skipFocus = false,
|
|
6647
6647
|
debug = () => {}
|
|
6648
6648
|
} = {}) => {
|
|
6649
|
-
import.meta.css = [css$
|
|
6649
|
+
import.meta.css = [css$11, "@jsenv/navi/src/control/rules/callout/callout.js"];
|
|
6650
6650
|
if (debug === true) {
|
|
6651
6651
|
debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
|
|
6652
6652
|
}
|
|
@@ -8009,7 +8009,14 @@ const isControlBusy = (field) => {
|
|
|
8009
8009
|
return true;
|
|
8010
8010
|
}
|
|
8011
8011
|
const { boundAction } = field;
|
|
8012
|
-
|
|
8012
|
+
// An optimistic control stays interactive while its bound action runs:
|
|
8013
|
+
// a new interaction is queued behind the run (see the action queue in
|
|
8014
|
+
// control_hooks.jsx) rather than refused.
|
|
8015
|
+
if (
|
|
8016
|
+
!field.optimistic &&
|
|
8017
|
+
boundAction &&
|
|
8018
|
+
boundAction.runningStateSignal.value === RUNNING
|
|
8019
|
+
) {
|
|
8013
8020
|
return true;
|
|
8014
8021
|
}
|
|
8015
8022
|
if (field.loadingFromParent) {
|
|
@@ -18866,7 +18873,7 @@ const setupNetworkMonitoring = () => {
|
|
|
18866
18873
|
};
|
|
18867
18874
|
setupNetworkMonitoring();
|
|
18868
18875
|
|
|
18869
|
-
installImportMetaCssBuild(import.meta);const css
|
|
18876
|
+
installImportMetaCssBuild(import.meta);const css$10 = /* css */`
|
|
18870
18877
|
.navi_loading_indicator_fluid_container {
|
|
18871
18878
|
position: relative;
|
|
18872
18879
|
display: flex;
|
|
@@ -18898,7 +18905,7 @@ const LoadingIndicatorFluid = ({
|
|
|
18898
18905
|
visuallyHidden,
|
|
18899
18906
|
...rest
|
|
18900
18907
|
}) => {
|
|
18901
|
-
import.meta.css = [css
|
|
18908
|
+
import.meta.css = [css$10, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
|
|
18902
18909
|
const ref = useRef(null);
|
|
18903
18910
|
// The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
|
|
18904
18911
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
@@ -19103,7 +19110,7 @@ const LoadingRectangleSvg = ({
|
|
|
19103
19110
|
});
|
|
19104
19111
|
};
|
|
19105
19112
|
|
|
19106
|
-
installImportMetaCssBuild(import.meta);const css
|
|
19113
|
+
installImportMetaCssBuild(import.meta);const css$$ = /* css */`
|
|
19107
19114
|
.navi_loading_outline_wrapper {
|
|
19108
19115
|
position: absolute;
|
|
19109
19116
|
/* Controls place the outline slightly outside their box, right on top of
|
|
@@ -19140,7 +19147,7 @@ installImportMetaCssBuild(import.meta);const css$_ = /* css */`
|
|
|
19140
19147
|
}
|
|
19141
19148
|
`;
|
|
19142
19149
|
const LoadingOutline = props => {
|
|
19143
|
-
import.meta.css = [css
|
|
19150
|
+
import.meta.css = [css$$, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
|
|
19144
19151
|
if (props.containerRef) {
|
|
19145
19152
|
const container = props.containerRef.current;
|
|
19146
19153
|
if (!container) {
|
|
@@ -19430,7 +19437,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
|
|
|
19430
19437
|
};
|
|
19431
19438
|
|
|
19432
19439
|
installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
|
|
19433
|
-
const css$
|
|
19440
|
+
const css$_ = /* css */`
|
|
19434
19441
|
@layer navi {
|
|
19435
19442
|
.navi_text {
|
|
19436
19443
|
&[data-skeleton] {
|
|
@@ -19951,7 +19958,7 @@ const TextShrinkWrap = props => {
|
|
|
19951
19958
|
});
|
|
19952
19959
|
};
|
|
19953
19960
|
const TextUI = props => {
|
|
19954
|
-
import.meta.css = [css$
|
|
19961
|
+
import.meta.css = [css$_, "@jsenv/navi/src/text/text.jsx"];
|
|
19955
19962
|
let {
|
|
19956
19963
|
ref,
|
|
19957
19964
|
spacing,
|
|
@@ -23198,6 +23205,7 @@ const CONTROL_PROP_SET = new Set([
|
|
|
23198
23205
|
"resetOnCancel",
|
|
23199
23206
|
"resetOnAbort",
|
|
23200
23207
|
"resetOnError",
|
|
23208
|
+
"optimistic",
|
|
23201
23209
|
|
|
23202
23210
|
"charGuard",
|
|
23203
23211
|
"maxLengthGuard",
|
|
@@ -24422,6 +24430,18 @@ const GROUP_DEFAULTS = {
|
|
|
24422
24430
|
if (isNamelessGrouping(child, child.uiState)) {
|
|
24423
24431
|
return newUIState;
|
|
24424
24432
|
}
|
|
24433
|
+
if (
|
|
24434
|
+
newUIState !== null &&
|
|
24435
|
+
typeof newUIState === "object" &&
|
|
24436
|
+
Object.keys(newUIState).length === 0
|
|
24437
|
+
) {
|
|
24438
|
+
// An object with nothing in it is not a partial answer, it is the
|
|
24439
|
+
// absence of one — a group being cleared — and every child empties. A
|
|
24440
|
+
// partial object leaves the children it does not name alone, which is
|
|
24441
|
+
// what CANNOT_DERIVE says below: a value that mentions `start` says
|
|
24442
|
+
// nothing about `end`, an empty one says there is no answer at all.
|
|
24443
|
+
return undefined;
|
|
24444
|
+
}
|
|
24425
24445
|
return CANNOT_DERIVE;
|
|
24426
24446
|
},
|
|
24427
24447
|
},
|
|
@@ -24747,9 +24767,6 @@ const useUIGroupStateController = (
|
|
|
24747
24767
|
if (!shouldPropagateStateToChild(childUIStateController)) {
|
|
24748
24768
|
return;
|
|
24749
24769
|
}
|
|
24750
|
-
if (childUIStateController.hasStateProp) {
|
|
24751
|
-
return;
|
|
24752
|
-
}
|
|
24753
24770
|
const childNewState = resolvedDistributeChildUIState(
|
|
24754
24771
|
groupUIState,
|
|
24755
24772
|
childUIStateController,
|
|
@@ -24757,6 +24774,22 @@ const useUIGroupStateController = (
|
|
|
24757
24774
|
if (childNewState === CANNOT_DERIVE) {
|
|
24758
24775
|
return;
|
|
24759
24776
|
}
|
|
24777
|
+
if (
|
|
24778
|
+
childUIStateController.hasStateProp &&
|
|
24779
|
+
!childUIStateController.props.signal
|
|
24780
|
+
) {
|
|
24781
|
+
// A child bound to a signal is placed like any other: bound is not
|
|
24782
|
+
// frozen, and the placement writes the signal, so both ends keep
|
|
24783
|
+
// saying the same thing. Only a child controlled by a `value` /
|
|
24784
|
+
// `checked` prop cannot be moved — its owner decides. Worth saying
|
|
24785
|
+
// out loud only when the two disagree: a child already showing what
|
|
24786
|
+
// the group would put there has lost nothing, and both being fed
|
|
24787
|
+
// from the same value is a legitimate way to write a group.
|
|
24788
|
+
if (
|
|
24789
|
+
!compareTwoJsValues(childNewState, childUIStateController.uiState)
|
|
24790
|
+
) ;
|
|
24791
|
+
return;
|
|
24792
|
+
}
|
|
24760
24793
|
childUIStateController.setUIState(childNewState, e);
|
|
24761
24794
|
},
|
|
24762
24795
|
setUIState: (newUIState, e) => {
|
|
@@ -25447,6 +25480,11 @@ const PROPAGATE_DOWN_EVENT_SET = new Set([
|
|
|
25447
25480
|
"propagate_down_set_ui_state",
|
|
25448
25481
|
"propagate_down_reset_ui_state",
|
|
25449
25482
|
"propagate_down_clear_ui_state",
|
|
25483
|
+
// The FIRST value handed down is one too: a control placed as it registers
|
|
25484
|
+
// (a group filling a child that just arrived, a picker filling its popup)
|
|
25485
|
+
// holds it from that moment, and a signal that kept saying nothing would have
|
|
25486
|
+
// the app and the screen disagree from the very first paint.
|
|
25487
|
+
"initial_state_push",
|
|
25450
25488
|
]);
|
|
25451
25489
|
const isPropagateDownEvent = (e) => {
|
|
25452
25490
|
return PROPAGATE_DOWN_EVENT_SET.has(e.type);
|
|
@@ -25518,17 +25556,21 @@ const resolveClearedUIState = (controller) => {
|
|
|
25518
25556
|
return "";
|
|
25519
25557
|
};
|
|
25520
25558
|
|
|
25521
|
-
// What a control says when it has nothing to say: no value at all,
|
|
25522
|
-
// array/object a group falls back to while it has no child to aggregate
|
|
25559
|
+
// What a control says when it has nothing to say: no value at all, the empty
|
|
25560
|
+
// array/object a group falls back to while it has no child to aggregate — or a
|
|
25561
|
+
// shape whose every part is itself nothing. That last one is what a group with
|
|
25562
|
+
// an aggregate of its own produces before its children have arrived
|
|
25563
|
+
// (`{ mode: undefined, levels: [] }` has two keys and says nothing), and
|
|
25564
|
+
// reading it as an answer is how a popup opening empties the row above it.
|
|
25523
25565
|
const uiStateHoldsNothing = (uiState) => {
|
|
25524
25566
|
if (uiState === undefined) {
|
|
25525
25567
|
return true;
|
|
25526
25568
|
}
|
|
25527
25569
|
if (Array.isArray(uiState)) {
|
|
25528
|
-
return uiState.
|
|
25570
|
+
return uiState.every(uiStateHoldsNothing);
|
|
25529
25571
|
}
|
|
25530
25572
|
if (uiState !== null && typeof uiState === "object") {
|
|
25531
|
-
return Object.
|
|
25573
|
+
return Object.values(uiState).every(uiStateHoldsNothing);
|
|
25532
25574
|
}
|
|
25533
25575
|
return false;
|
|
25534
25576
|
};
|
|
@@ -26814,7 +26856,8 @@ const useInteractiveProps = (props, {
|
|
|
26814
26856
|
disabled,
|
|
26815
26857
|
required,
|
|
26816
26858
|
readOnly,
|
|
26817
|
-
loading
|
|
26859
|
+
loading,
|
|
26860
|
+
optimistic
|
|
26818
26861
|
} = props;
|
|
26819
26862
|
const disabledResolved = disabled || controlDisabled;
|
|
26820
26863
|
const requiredResolved = required || controlRequired;
|
|
@@ -26829,14 +26872,22 @@ const useInteractiveProps = (props, {
|
|
|
26829
26872
|
// but cannot be taken.
|
|
26830
26873
|
const readOnlyFromParentMaxLengthGuard = Boolean(uiStateController.parentUIStateController?.isChildBlockedByMaxLengthGuard?.(uiStateController));
|
|
26831
26874
|
const readOnlyBase = readOnly || controlReadOnly || loadingBase || readOnlyFromParentMaxLengthGuard || controlInfo.readOnlyUncontrolled;
|
|
26832
|
-
|
|
26833
|
-
|
|
26875
|
+
// An optimistic control trusts its action to succeed: the state the user
|
|
26876
|
+
// just set stays visible and interactive while the action runs — no
|
|
26877
|
+
// loading, no readonly. On failure resetOnError rolls the state back and
|
|
26878
|
+
// the error callout says why.
|
|
26879
|
+
const actionLoading = optimistic ? false : actionStatus.loading;
|
|
26880
|
+
const loadingResolved = loadingBase || actionLoading;
|
|
26881
|
+
const readOnlyResolved = readOnlyBase || actionLoading;
|
|
26834
26882
|
// Both halves of "busy" that do not come from the bound action, kept apart
|
|
26835
26883
|
// from each other and from it: BUSY_CONSTRAINT answers each from its own
|
|
26836
26884
|
// live source rather than from the rendered aria-busy, which conflates all
|
|
26837
26885
|
// three and is a frame behind. See its own comment.
|
|
26838
26886
|
uiStateController.loadingFromOwnProp = Boolean(loading);
|
|
26839
26887
|
uiStateController.loadingFromParent = loadingFromParent;
|
|
26888
|
+
// Read by BUSY_CONSTRAINT: an optimistic control stays interactive while
|
|
26889
|
+
// its bound action runs (a new toggle replaces the run instead of waiting).
|
|
26890
|
+
uiStateController.optimistic = Boolean(optimistic);
|
|
26840
26891
|
Object.assign(controlHostProps, {
|
|
26841
26892
|
"required": requiredResolved,
|
|
26842
26893
|
"aria-busy": loadingResolved ? "true" : "false",
|
|
@@ -27002,7 +27053,8 @@ const useInteractiveProps = (props, {
|
|
|
27002
27053
|
onActionEnd,
|
|
27003
27054
|
resetOnCancel,
|
|
27004
27055
|
resetOnAbort,
|
|
27005
|
-
resetOnError
|
|
27056
|
+
resetOnError,
|
|
27057
|
+
optimistic
|
|
27006
27058
|
} = props;
|
|
27007
27059
|
Object.assign(controlHostProps, {
|
|
27008
27060
|
onFocus: e => {
|
|
@@ -27057,14 +27109,80 @@ const useInteractiveProps = (props, {
|
|
|
27057
27109
|
// special case for the use case where form.requestSubmit is called
|
|
27058
27110
|
e.detail.action = boundAction;
|
|
27059
27111
|
}
|
|
27112
|
+
// An optimistic control stays interactive while its action runs, so a
|
|
27113
|
+
// second request can arrive mid-run. The server must receive them in
|
|
27114
|
+
// order: the new request is queued (latest wins — the bound action
|
|
27115
|
+
// reads the UI state signal at run time, so what goes out is always
|
|
27116
|
+
// the current state) and goes out once the running one has truly
|
|
27117
|
+
// settled (see onnavi_action_start). The running action is asked to
|
|
27118
|
+
// abort — its outcome is already outdated — but aborting is only a
|
|
27119
|
+
// resource optimization (a fetch wired to the action's signal gets
|
|
27120
|
+
// cancelled): the server may have done the work anyway, and only the
|
|
27121
|
+
// settlement says which. So even aborted, the underlying work is
|
|
27122
|
+
// awaited before the queued request runs. See the abort section in
|
|
27123
|
+
// docs/actions.md.
|
|
27124
|
+
if (optimistic && uiStateController.actionInFlight) {
|
|
27125
|
+
debugAction(e, `queueing action (one already in flight)`);
|
|
27126
|
+
uiStateController.queuedActionAllowedEvent = e;
|
|
27127
|
+
// The instance captured at navi_action_start, NOT boundAction:
|
|
27128
|
+
// boundAction is a proxy following the UI state signal, and the UI
|
|
27129
|
+
// state has already moved to the new value by now — the proxy would
|
|
27130
|
+
// resolve to the instance for that new value, which is not the one
|
|
27131
|
+
// running.
|
|
27132
|
+
uiStateController.runningAction?.abort(`superseded by a newer request on this control`);
|
|
27133
|
+
return;
|
|
27134
|
+
}
|
|
27060
27135
|
debugAction(e, `executing action ${e.detail.action.callSource}`);
|
|
27061
27136
|
executeAction(e);
|
|
27062
27137
|
},
|
|
27063
27138
|
onnavi_action_start: e => {
|
|
27139
|
+
// The run this control currently waits on, identified by the
|
|
27140
|
+
// navi_action_allowed event that launched it (unique per execution,
|
|
27141
|
+
// carried by every navi_action_* event of that run).
|
|
27142
|
+
uiStateController.pendingActionEvent = e.detail.event;
|
|
27143
|
+
uiStateController.actionInFlight = true;
|
|
27144
|
+
// The very instance this run uses, resolved now — while the UI state
|
|
27145
|
+
// still holds the value the run was made for. detail.action may be a
|
|
27146
|
+
// proxy following that state, and by the time anyone wants to abort
|
|
27147
|
+
// this run (see the optimistic queue above), the state — and the
|
|
27148
|
+
// proxy's resolution — will have moved on.
|
|
27149
|
+
const runAction = e.detail.action;
|
|
27150
|
+
uiStateController.runningAction = runAction.getCurrentAction?.() ?? runAction;
|
|
27151
|
+
// Fires when the run's underlying work has settled — even for an
|
|
27152
|
+
// aborted run, whose promise is awaited to completion (see
|
|
27153
|
+
// performRun in actions.js) — which is exactly what "the server is
|
|
27154
|
+
// done with it" means, and therefore when the queued request may go.
|
|
27155
|
+
e.detail.addSideEffect(outcome => {
|
|
27156
|
+
uiStateController.actionInFlight = false;
|
|
27157
|
+
uiStateController.runningAction = null;
|
|
27158
|
+
const queuedEvent = uiStateController.queuedActionAllowedEvent;
|
|
27159
|
+
uiStateController.queuedActionAllowedEvent = null;
|
|
27160
|
+
if (!queuedEvent) {
|
|
27161
|
+
return;
|
|
27162
|
+
}
|
|
27163
|
+
if (outcome.error) {
|
|
27164
|
+
// A failure abandons the queue: the UI is rolled back to the last
|
|
27165
|
+
// known state (resetOnError above), and what was queued was built
|
|
27166
|
+
// on top of the state that just failed.
|
|
27167
|
+
return;
|
|
27168
|
+
}
|
|
27169
|
+
// A microtask later, not right here: this runs inside the batch()
|
|
27170
|
+
// that settles the action (see watchActionCompletion for the same
|
|
27171
|
+
// constraint).
|
|
27172
|
+
queueMicrotask(() => {
|
|
27173
|
+
executeAction(queuedEvent);
|
|
27174
|
+
});
|
|
27175
|
+
});
|
|
27064
27176
|
onActionStart?.(e);
|
|
27065
27177
|
},
|
|
27066
27178
|
onnavi_action_abort: e => {
|
|
27067
|
-
|
|
27179
|
+
// Only an abort that leaves the control with nothing left to do may
|
|
27180
|
+
// reset the UI state. An abort whose run was superseded — a queued
|
|
27181
|
+
// request waits behind it (optimistic), or a newer run already
|
|
27182
|
+
// started — must leave the state alone: it belongs to the newer
|
|
27183
|
+
// request, resetting would throw away what the user just set.
|
|
27184
|
+
const superseded = Boolean(uiStateController.queuedActionAllowedEvent) || e.detail.event !== uiStateController.pendingActionEvent;
|
|
27185
|
+
if (resetOnAbort && !superseded) {
|
|
27068
27186
|
dispatchRequestResetUIState(e.currentTarget, e);
|
|
27069
27187
|
}
|
|
27070
27188
|
onActionAborted?.(e);
|
|
@@ -27162,7 +27280,7 @@ const getAssociatedLabels = element => {
|
|
|
27162
27280
|
return Array.from(element.labels);
|
|
27163
27281
|
};
|
|
27164
27282
|
|
|
27165
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
27283
|
+
installImportMetaCssBuild(import.meta);const css$Z = /* css */`
|
|
27166
27284
|
@layer navi {
|
|
27167
27285
|
.navi_button {
|
|
27168
27286
|
--button-border-radius: var(--navi-control-border-radius);
|
|
@@ -27593,7 +27711,7 @@ installImportMetaCssBuild(import.meta);const css$Y = /* css */`
|
|
|
27593
27711
|
}
|
|
27594
27712
|
`;
|
|
27595
27713
|
const ButtonUI = props => {
|
|
27596
|
-
import.meta.css = [css$
|
|
27714
|
+
import.meta.css = [css$Z, "@jsenv/navi/src/control/input/button_ui.jsx"];
|
|
27597
27715
|
const {
|
|
27598
27716
|
ref,
|
|
27599
27717
|
// href/link
|
|
@@ -28092,6 +28210,7 @@ const createOpenController = (
|
|
|
28092
28210
|
// Last: the close effects above are what starts the exit transition the
|
|
28093
28211
|
// content must outlive (see popup_content_mount.js).
|
|
28094
28212
|
controller.unmountContent?.();
|
|
28213
|
+
controller.onOpenedChange?.(false);
|
|
28095
28214
|
};
|
|
28096
28215
|
const controller = {
|
|
28097
28216
|
opened: false,
|
|
@@ -28110,6 +28229,11 @@ const createOpenController = (
|
|
|
28110
28229
|
// The counterpart, set only when the popup was told to throw its content
|
|
28111
28230
|
// away on close (`unmountWhenClosed`). Called from performClose above.
|
|
28112
28231
|
unmountContent: null,
|
|
28232
|
+
// Told whenever `opened` actually changes, whatever asked for it — an
|
|
28233
|
+
// interaction, a command, a prop. What lets a `signal` prop reflect the
|
|
28234
|
+
// popup's real state (see useOpenPropsEffectOnOpenController), called once
|
|
28235
|
+
// the open/close has fully happened rather than mid-sequence.
|
|
28236
|
+
onOpenedChange: null,
|
|
28113
28237
|
open: (e, detail) => {
|
|
28114
28238
|
if (controller.opened || !controller.openEffect) {
|
|
28115
28239
|
return;
|
|
@@ -28212,6 +28336,7 @@ const createOpenController = (
|
|
|
28212
28336
|
openEffectReturnValue?.(closeEvent);
|
|
28213
28337
|
};
|
|
28214
28338
|
closeHandlers = openHandler(requestOpenEvent) || null;
|
|
28339
|
+
controller.onOpenedChange?.(true);
|
|
28215
28340
|
},
|
|
28216
28341
|
requestClose: (
|
|
28217
28342
|
e = new CustomEvent("programmatic", { detail: {} }),
|
|
@@ -28340,10 +28465,23 @@ const scheduleMountOpen = (run) => {
|
|
|
28340
28465
|
* `requestOpen`/`requestClose` wrappers).
|
|
28341
28466
|
*
|
|
28342
28467
|
* @param {{ open: (e: Event, detail?: object) => void, requestClose: (e: Event, detail?: object) => void, opened: boolean }} openController
|
|
28343
|
-
* @param {{ open?: boolean|"interaction", defaultOpen?: boolean|"interaction" }} props
|
|
28468
|
+
* @param {{ open?: boolean|"interaction", defaultOpen?: boolean|"interaction", signal?: import("@preact/signals").Signal<boolean> }} props
|
|
28344
28469
|
*/
|
|
28345
28470
|
const useOpenPropsEffectOnOpenController = (openController, props) => {
|
|
28346
|
-
const {
|
|
28471
|
+
const { signal, defaultOpen } = props;
|
|
28472
|
+
// What the caller holds, however they hold it: an `open` they re-render
|
|
28473
|
+
// themselves, or a `signal` this hook also writes (see onOpenedChange below).
|
|
28474
|
+
// Reading .value during render is what subscribes the popup to it.
|
|
28475
|
+
const open = signal ? signal.value : props.open;
|
|
28476
|
+
// Assigned on every render, like openEffect, so it always closes over the
|
|
28477
|
+
// latest prop: a popup that opens or closes on its own (Escape, backdrop, a
|
|
28478
|
+
// --navi-close command) writes what happened into the signal, so whoever
|
|
28479
|
+
// holds it always reads where the popup is.
|
|
28480
|
+
openController.onOpenedChange = signal
|
|
28481
|
+
? (opened) => {
|
|
28482
|
+
signal.value = opened;
|
|
28483
|
+
}
|
|
28484
|
+
: null;
|
|
28347
28485
|
// Tracks whether the effect below has ever run before — only the very
|
|
28348
28486
|
// first run gets the "mount already open" treatment (`open` truthy from
|
|
28349
28487
|
// the start, or the uncontrolled, mount-only `defaultOpen`); every
|
|
@@ -28396,6 +28534,12 @@ const useOpenPropsEffectOnOpenController = (openController, props) => {
|
|
|
28396
28534
|
{ isCancel: true },
|
|
28397
28535
|
);
|
|
28398
28536
|
}
|
|
28537
|
+
if (signal) {
|
|
28538
|
+
// The request can be refused (a busy form denying the close): the popup
|
|
28539
|
+
// then stays where it was, and the signal is told so — otherwise it
|
|
28540
|
+
// would keep saying "closed" about a popup still open.
|
|
28541
|
+
signal.value = openController.opened;
|
|
28542
|
+
}
|
|
28399
28543
|
}, [open]);
|
|
28400
28544
|
};
|
|
28401
28545
|
|
|
@@ -29278,7 +29422,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
29278
29422
|
* reaches the real container.
|
|
29279
29423
|
*/
|
|
29280
29424
|
let openLocalDialogCount = 0;
|
|
29281
|
-
const css$
|
|
29425
|
+
const css$Y = /* css */`
|
|
29282
29426
|
@layer navi {
|
|
29283
29427
|
.navi_dialog {
|
|
29284
29428
|
/* Min gap between the dialog and the edges of its container. Written
|
|
@@ -29820,6 +29964,14 @@ const css$X = /* css */`
|
|
|
29820
29964
|
* the focus only leaves it for something that asked by name (`autoFocus` on
|
|
29821
29965
|
* that element, which outranks whatever the dialog says).
|
|
29822
29966
|
* @param {boolean} [props.open] - Controlled open state.
|
|
29967
|
+
* @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
|
|
29968
|
+
* state said the way every navi control says it: the dialog opens and closes
|
|
29969
|
+
* to match the signal, and writes into it whenever it opens or closes on its
|
|
29970
|
+
* own (Escape, backdrop, a --navi-close command) — one binding to both drive
|
|
29971
|
+
* the dialog and know where it is, and the state stays where the app put it.
|
|
29972
|
+
* Excludes `open`; `onOpen`/`onClose` still fire. A signal holding `true` at
|
|
29973
|
+
* mount behaves like `defaultOpen`: the dialog was already open, no entrance
|
|
29974
|
+
* plays.
|
|
29823
29975
|
* @param {boolean|"interaction"} [props.defaultOpen] - Uncontrolled, mount-only
|
|
29824
29976
|
* initial open state. `true` plays no entrance animation: the dialog was
|
|
29825
29977
|
* already open when the page appeared, and nothing was ever shown as "closed"
|
|
@@ -29854,7 +30006,7 @@ const css$X = /* css */`
|
|
|
29854
30006
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
29855
30007
|
*/
|
|
29856
30008
|
const Dialog = props => {
|
|
29857
|
-
import.meta.css = [css$
|
|
30009
|
+
import.meta.css = [css$Y, "@jsenv/navi/src/layout/dialog.jsx"];
|
|
29858
30010
|
if (props.openController) {
|
|
29859
30011
|
return jsx(ControlledDialog, {
|
|
29860
30012
|
...props
|
|
@@ -29902,6 +30054,7 @@ const UncontrolledDialog = props => {
|
|
|
29902
30054
|
return jsx(ControlledDialog, {
|
|
29903
30055
|
...props,
|
|
29904
30056
|
open: undefined,
|
|
30057
|
+
signal: undefined,
|
|
29905
30058
|
defaultOpen: undefined,
|
|
29906
30059
|
onClose: undefined,
|
|
29907
30060
|
openController: openController,
|
|
@@ -30845,7 +30998,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
30845
30998
|
* and applied.
|
|
30846
30999
|
*/
|
|
30847
31000
|
let openLocalPopoverCount = 0;
|
|
30848
|
-
const css$
|
|
31001
|
+
const css$X = /* css */`
|
|
30849
31002
|
@layer navi {
|
|
30850
31003
|
.navi_popover {
|
|
30851
31004
|
/* soft: user-configurable preferred max-height. Kept as a *default*
|
|
@@ -31259,6 +31412,14 @@ const css$W = /* css */`
|
|
|
31259
31412
|
* the focus only leaves it for something that asked by name (`autoFocus` on
|
|
31260
31413
|
* that element, which outranks whatever the popover says).
|
|
31261
31414
|
* @param {boolean} [props.open] - Controlled open state.
|
|
31415
|
+
* @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
|
|
31416
|
+
* state said the way every navi control says it: the popover opens and closes
|
|
31417
|
+
* to match the signal, and writes into it whenever it opens or closes on its
|
|
31418
|
+
* own (Escape, light dismiss, a --navi-close command) — one binding to both
|
|
31419
|
+
* drive the popover and know where it is, and the state stays where the app
|
|
31420
|
+
* put it. Excludes `open`; `onOpen`/`onClose` still fire. A signal holding
|
|
31421
|
+
* `true` at mount behaves like `defaultOpen`: the popover was already open,
|
|
31422
|
+
* no entrance plays.
|
|
31262
31423
|
* @param {boolean|"interaction"} [props.defaultOpen] - Uncontrolled, mount-only
|
|
31263
31424
|
* initial open state. `true` plays no entrance animation: the popover was
|
|
31264
31425
|
* already open when the page appeared, and nothing was ever shown as "closed"
|
|
@@ -31293,7 +31454,7 @@ const css$W = /* css */`
|
|
|
31293
31454
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
31294
31455
|
*/
|
|
31295
31456
|
const Popover = props => {
|
|
31296
|
-
import.meta.css = [css$
|
|
31457
|
+
import.meta.css = [css$X, "@jsenv/navi/src/layout/popover.jsx"];
|
|
31297
31458
|
if (props.openController) {
|
|
31298
31459
|
return jsx(ControlledPopover, {
|
|
31299
31460
|
...props
|
|
@@ -31339,6 +31500,7 @@ const UncontrolledPopover = props => {
|
|
|
31339
31500
|
return jsx(ControlledPopover, {
|
|
31340
31501
|
...props,
|
|
31341
31502
|
open: undefined,
|
|
31503
|
+
signal: undefined,
|
|
31342
31504
|
defaultOpen: undefined,
|
|
31343
31505
|
onClose: undefined,
|
|
31344
31506
|
openController: openController,
|
|
@@ -32320,7 +32482,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
32320
32482
|
* event, and a caller replacing the body entirely then has one protocol to
|
|
32321
32483
|
* follow — `--navi-confirm` for yes, anything that closes for no.
|
|
32322
32484
|
*/
|
|
32323
|
-
const css$
|
|
32485
|
+
const css$W = /* css */`
|
|
32324
32486
|
/* The width lives on the body rather than on the popup, so that custom
|
|
32325
32487
|
content (which replaces this body entirely) sizes itself instead of
|
|
32326
32488
|
inheriting a ceiling meant for a sentence-long question. */
|
|
@@ -32457,7 +32619,7 @@ const ConfirmPopup = ({
|
|
|
32457
32619
|
onAnswer,
|
|
32458
32620
|
onClosed
|
|
32459
32621
|
}) => {
|
|
32460
|
-
import.meta.css = [css$
|
|
32622
|
+
import.meta.css = [css$W, "@jsenv/navi/src/action/confirm_popup.jsx"];
|
|
32461
32623
|
const {
|
|
32462
32624
|
mode,
|
|
32463
32625
|
confirmLabel,
|
|
@@ -32541,7 +32703,7 @@ const defaultBody = (message, {
|
|
|
32541
32703
|
});
|
|
32542
32704
|
};
|
|
32543
32705
|
|
|
32544
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
32706
|
+
installImportMetaCssBuild(import.meta);const css$V = /* css */`
|
|
32545
32707
|
.action_error {
|
|
32546
32708
|
margin-top: 0;
|
|
32547
32709
|
margin-bottom: 20px;
|
|
@@ -32566,7 +32728,7 @@ const ActionRenderer = ({
|
|
|
32566
32728
|
children,
|
|
32567
32729
|
disabled
|
|
32568
32730
|
}) => {
|
|
32569
|
-
import.meta.css = [css$
|
|
32731
|
+
import.meta.css = [css$V, "@jsenv/navi/src/action/action_renderer.jsx"];
|
|
32570
32732
|
if (action === undefined) {
|
|
32571
32733
|
throw new Error("ActionRenderer requires an action to render, but none was provided.");
|
|
32572
32734
|
}
|
|
@@ -38310,7 +38472,7 @@ const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
|
|
|
38310
38472
|
// the root pictures must NOT move (they carry the whole viewport, blank bands
|
|
38311
38473
|
// included).
|
|
38312
38474
|
|
|
38313
|
-
const css$
|
|
38475
|
+
const css$U = /* css */`
|
|
38314
38476
|
/* The marked region is a picture of its own for the length of a transition of
|
|
38315
38477
|
OURS, and only then — the name is what makes the pages a picture the
|
|
38316
38478
|
movement below can carry.
|
|
@@ -38739,7 +38901,7 @@ const RouteTransitionArea = ({
|
|
|
38739
38901
|
children,
|
|
38740
38902
|
...rest
|
|
38741
38903
|
}) => {
|
|
38742
|
-
import.meta.css = [css$
|
|
38904
|
+
import.meta.css = [css$U, "@jsenv/navi/src/nav/route_transition.jsx"];
|
|
38743
38905
|
const props = {
|
|
38744
38906
|
...rest,
|
|
38745
38907
|
[TRANSITION_AREA_ATTRIBUTE]: ""
|
|
@@ -38796,7 +38958,7 @@ const RouteTransitionArea = ({
|
|
|
38796
38958
|
* @returns {() => void} remove this relation.
|
|
38797
38959
|
*/
|
|
38798
38960
|
const defineRouteTransition = (from, to, transition) => {
|
|
38799
|
-
import.meta.css = [css$
|
|
38961
|
+
import.meta.css = [css$U, "@jsenv/navi/src/nav/route_transition.jsx"];
|
|
38800
38962
|
const {
|
|
38801
38963
|
type,
|
|
38802
38964
|
duration
|
|
@@ -38832,7 +38994,7 @@ const defineRouteTransition = (from, to, transition) => {
|
|
|
38832
38994
|
* @returns {() => void} remove this default.
|
|
38833
38995
|
*/
|
|
38834
38996
|
const defineRouteDefaultTransition = transition => {
|
|
38835
|
-
import.meta.css = [css$
|
|
38997
|
+
import.meta.css = [css$U, "@jsenv/navi/src/nav/route_transition.jsx"];
|
|
38836
38998
|
const value = normalizeTransition(transition);
|
|
38837
38999
|
defaultTransition = value;
|
|
38838
39000
|
return () => {
|
|
@@ -39433,7 +39595,7 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
|
|
|
39433
39595
|
const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
|
|
39434
39596
|
// The name the box wears while it travels, and only then (see nameForTravel).
|
|
39435
39597
|
const TRAVEL_NAME = "navi-route-travel";
|
|
39436
|
-
const css$
|
|
39598
|
+
const css$T = /* css */`
|
|
39437
39599
|
/* The name that makes the page inside this box a picture of its own during a
|
|
39438
39600
|
transition — rather than part of the one big picture the document takes, so
|
|
39439
39601
|
the two pages can move past each other while everything else stays where it
|
|
@@ -39823,7 +39985,7 @@ const RouteTravel = ({
|
|
|
39823
39985
|
children,
|
|
39824
39986
|
...rest
|
|
39825
39987
|
}) => {
|
|
39826
|
-
import.meta.css = [css$
|
|
39988
|
+
import.meta.css = [css$T, "@jsenv/navi/src/nav/route_travel.jsx"];
|
|
39827
39989
|
const elementRef = useRef();
|
|
39828
39990
|
const gestureRef = useRef(null);
|
|
39829
39991
|
// The travel in hand: the transition keeping the picture of the page being
|
|
@@ -42385,7 +42547,7 @@ const PhoneSvg = () => {
|
|
|
42385
42547
|
};
|
|
42386
42548
|
|
|
42387
42549
|
installImportMetaCssBuild(import.meta);// # TextAnchor — how it works
|
|
42388
|
-
const css$
|
|
42550
|
+
const css$S = /* css */`
|
|
42389
42551
|
.navi_text_anchor {
|
|
42390
42552
|
vertical-align: baseline;
|
|
42391
42553
|
user-select: none;
|
|
@@ -42420,7 +42582,7 @@ const TextAnchor = ({
|
|
|
42420
42582
|
textSize,
|
|
42421
42583
|
lineLayout
|
|
42422
42584
|
}) => {
|
|
42423
|
-
import.meta.css = [css$
|
|
42585
|
+
import.meta.css = [css$S, "@jsenv/navi/src/text/text_anchor.jsx"];
|
|
42424
42586
|
const anchorRef = useRef();
|
|
42425
42587
|
|
|
42426
42588
|
// Plain useLayoutEffect would also fire while an ancestor dialog/popover
|
|
@@ -42535,7 +42697,7 @@ const computeTopOffset = ({
|
|
|
42535
42697
|
};
|
|
42536
42698
|
const charTopCanvas = document.createElement("canvas");
|
|
42537
42699
|
|
|
42538
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
42700
|
+
installImportMetaCssBuild(import.meta);const css$R = /* css */`
|
|
42539
42701
|
@layer navi {
|
|
42540
42702
|
/* Ensure data attributes from box.jsx can win to update display */
|
|
42541
42703
|
.navi_icon {
|
|
@@ -42693,7 +42855,7 @@ const Icon = ({
|
|
|
42693
42855
|
fillLine,
|
|
42694
42856
|
...props
|
|
42695
42857
|
}) => {
|
|
42696
|
-
import.meta.css = [css$
|
|
42858
|
+
import.meta.css = [css$R, "@jsenv/navi/src/text/icon.jsx"];
|
|
42697
42859
|
const innerChildren = href ? jsx("svg", {
|
|
42698
42860
|
width: "100%",
|
|
42699
42861
|
height: "100%",
|
|
@@ -42855,7 +43017,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
|
|
|
42855
43017
|
});
|
|
42856
43018
|
};
|
|
42857
43019
|
|
|
42858
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
43020
|
+
installImportMetaCssBuild(import.meta);const css$Q = /* css */`
|
|
42859
43021
|
@layer navi {
|
|
42860
43022
|
.navi_link {
|
|
42861
43023
|
--link-border-radius: unset;
|
|
@@ -43317,7 +43479,7 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
43317
43479
|
* @param {boolean} [props.readOnly]
|
|
43318
43480
|
*/
|
|
43319
43481
|
const Link = props => {
|
|
43320
|
-
import.meta.css = [css$
|
|
43482
|
+
import.meta.css = [css$Q, "@jsenv/navi/src/nav/link/link.jsx"];
|
|
43321
43483
|
if (props.route) {
|
|
43322
43484
|
return jsx(LinkWithRoute, {
|
|
43323
43485
|
...props
|
|
@@ -43640,7 +43802,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
43640
43802
|
* https://dribbble.com/search/tabs
|
|
43641
43803
|
*/
|
|
43642
43804
|
let navCount = 0;
|
|
43643
|
-
const css$
|
|
43805
|
+
const css$P = /* css */`
|
|
43644
43806
|
@layer navi {
|
|
43645
43807
|
.navi_nav {
|
|
43646
43808
|
--nav-border: none;
|
|
@@ -43923,7 +44085,7 @@ const Nav = ({
|
|
|
43923
44085
|
slideContainer,
|
|
43924
44086
|
...props
|
|
43925
44087
|
}) => {
|
|
43926
|
-
import.meta.css = [css$
|
|
44088
|
+
import.meta.css = [css$P, "@jsenv/navi/src/nav/link/nav.jsx"];
|
|
43927
44089
|
const defaultRef = useRef();
|
|
43928
44090
|
props.ref = props.ref || defaultRef;
|
|
43929
44091
|
const navRef = props.ref;
|
|
@@ -44450,7 +44612,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
44450
44612
|
* Border width participates in layout (it is added to the tab and page
|
|
44451
44613
|
* padding): a thick border grows the binder rather than eating into the text.
|
|
44452
44614
|
*/
|
|
44453
|
-
const css$
|
|
44615
|
+
const css$O = /* css */`
|
|
44454
44616
|
@layer navi {
|
|
44455
44617
|
.navi_binder {
|
|
44456
44618
|
--binder-border-width: var(--navi-control-border-width);
|
|
@@ -44763,7 +44925,7 @@ const Binder = ({
|
|
|
44763
44925
|
pagePadding,
|
|
44764
44926
|
...props
|
|
44765
44927
|
}) => {
|
|
44766
|
-
import.meta.css = [css$
|
|
44928
|
+
import.meta.css = [css$O, "@jsenv/navi/src/nav/binder/binder.jsx"];
|
|
44767
44929
|
const items = toChildArray(children).map((child, index) => {
|
|
44768
44930
|
const {
|
|
44769
44931
|
value: itemValue,
|
|
@@ -45250,7 +45412,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
45250
45412
|
* added to the size asked for exactly like the notch inset is, so the
|
|
45251
45413
|
* content still gets the size the prop names.
|
|
45252
45414
|
*/
|
|
45253
|
-
const css$
|
|
45415
|
+
const css$N = /* css */`
|
|
45254
45416
|
@layer navi {
|
|
45255
45417
|
:root {
|
|
45256
45418
|
--navi-fixed-bar-width: 56px;
|
|
@@ -45398,7 +45560,7 @@ const FixedBar = ({
|
|
|
45398
45560
|
border = true,
|
|
45399
45561
|
...props
|
|
45400
45562
|
}) => {
|
|
45401
|
-
import.meta.css = [css$
|
|
45563
|
+
import.meta.css = [css$N, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
|
|
45402
45564
|
const defaultRef = useRef();
|
|
45403
45565
|
props.ref = props.ref || defaultRef;
|
|
45404
45566
|
// Said with the width the border rule reads rather than with an attribute of
|
|
@@ -45492,7 +45654,7 @@ const FixedBar = ({
|
|
|
45492
45654
|
// Subpixel layout rounds rectangles up on boxes that fit exactly.
|
|
45493
45655
|
const OVERFLOW_TOLERANCE = 1;
|
|
45494
45656
|
|
|
45495
|
-
const css$
|
|
45657
|
+
const css$M = /* css */ `
|
|
45496
45658
|
[data-navi-overflow-x] {
|
|
45497
45659
|
outline: 2px dashed #e74c3c;
|
|
45498
45660
|
outline-offset: -2px;
|
|
@@ -45516,7 +45678,7 @@ const detectHorizontalOverflow = ({
|
|
|
45516
45678
|
let styleEl = null;
|
|
45517
45679
|
if (highlight) {
|
|
45518
45680
|
styleEl = document.createElement("style");
|
|
45519
|
-
styleEl.textContent = css$
|
|
45681
|
+
styleEl.textContent = css$M;
|
|
45520
45682
|
document.head.appendChild(styleEl);
|
|
45521
45683
|
}
|
|
45522
45684
|
|
|
@@ -45672,7 +45834,7 @@ const useFocusGroup = (
|
|
|
45672
45834
|
|
|
45673
45835
|
installImportMetaCssBuild(import.meta);const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
|
|
45674
45836
|
const downArrowPath = "M480-280L160-600l80-80 240 240 240-240 80 80-320 320z";
|
|
45675
|
-
const css$
|
|
45837
|
+
const css$L = /* css */`
|
|
45676
45838
|
.navi_summary_marker {
|
|
45677
45839
|
width: 1em;
|
|
45678
45840
|
height: 1em;
|
|
@@ -45757,7 +45919,7 @@ const SummaryMarker = ({
|
|
|
45757
45919
|
open,
|
|
45758
45920
|
loading
|
|
45759
45921
|
}) => {
|
|
45760
|
-
import.meta.css = [css$
|
|
45922
|
+
import.meta.css = [css$L, "@jsenv/navi/src/control/details/summary_marker.jsx"];
|
|
45761
45923
|
const showLoading = useDebounceTrue(loading, 300);
|
|
45762
45924
|
const mountedRef = useRef(false);
|
|
45763
45925
|
const prevOpenRef = useRef(open);
|
|
@@ -45811,7 +45973,7 @@ const SummaryMarker = ({
|
|
|
45811
45973
|
});
|
|
45812
45974
|
};
|
|
45813
45975
|
|
|
45814
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
45976
|
+
installImportMetaCssBuild(import.meta);const css$K = /* css */`
|
|
45815
45977
|
.navi_details {
|
|
45816
45978
|
position: relative;
|
|
45817
45979
|
z-index: 1;
|
|
@@ -45857,7 +46019,7 @@ const Details = props => {
|
|
|
45857
46019
|
return details;
|
|
45858
46020
|
};
|
|
45859
46021
|
const DetailsField = props => {
|
|
45860
|
-
import.meta.css = [css$
|
|
46022
|
+
import.meta.css = [css$K, "@jsenv/navi/src/control/details/details.jsx"];
|
|
45861
46023
|
const {
|
|
45862
46024
|
ref,
|
|
45863
46025
|
persists,
|
|
@@ -46013,12 +46175,647 @@ const DetailsFieldContent = ({
|
|
|
46013
46175
|
children
|
|
46014
46176
|
}) => {
|
|
46015
46177
|
const action = useContext(ActionContext);
|
|
46178
|
+
if (!action) {
|
|
46179
|
+
return children;
|
|
46180
|
+
}
|
|
46016
46181
|
return jsx(ActionRenderer, {
|
|
46017
46182
|
action: action,
|
|
46018
46183
|
children: children
|
|
46019
46184
|
});
|
|
46020
46185
|
};
|
|
46021
46186
|
|
|
46187
|
+
installImportMetaCssBuild(import.meta);/**
|
|
46188
|
+
* Expandable: an in-flow disclosure — a UI part that reveals a content part.
|
|
46189
|
+
* It covers the same ground as <Details> with structural differences:
|
|
46190
|
+
*
|
|
46191
|
+
* - the two parts are explicit and free to order/orient:
|
|
46192
|
+
*
|
|
46193
|
+
* <Expandable>
|
|
46194
|
+
* <Expandable.UI>See more</Expandable.UI>
|
|
46195
|
+
* <Expandable.Content>…</Expandable.Content>
|
|
46196
|
+
* </Expandable>
|
|
46197
|
+
*
|
|
46198
|
+
* Content after UI expands below (the <details> shape), Content before UI
|
|
46199
|
+
* expands above; `layout="column"` puts the parts side by side instead, the
|
|
46200
|
+
* content then expanding horizontally. The common shape has a shorthand:
|
|
46201
|
+
* `ui` prop + children as content.
|
|
46202
|
+
* - the UI part accepts any markup (buttons, links, fields) — only the small
|
|
46203
|
+
* marker is a real <button>, carrying the aria for the whole row
|
|
46204
|
+
* (aria-expanded/aria-controls, labelled by the UI part), so nested
|
|
46205
|
+
* interactive content never ends up inside an interactive element.
|
|
46206
|
+
*
|
|
46207
|
+
* What <details> gives for free is rebuilt here:
|
|
46208
|
+
* - a "toggle" event (a real ToggleEvent when the browser has it) dispatched on
|
|
46209
|
+
* the root whenever the state actually changes — but never on mount, unlike
|
|
46210
|
+
* the native one (see the workaround comment in details.jsx);
|
|
46211
|
+
* - `--navi-toggle`/`--navi-open`/`--navi-close` commands work against it: the
|
|
46212
|
+
* root carries `aria-expanded` (what the command system reads) and answers
|
|
46213
|
+
* the `navi_command`/`navi_request_open`/`navi_request_close` events.
|
|
46214
|
+
*
|
|
46215
|
+
* Content is not built until the first expansion and stays built afterwards —
|
|
46216
|
+
* same policy, same prop names as popups (see popup_content_mount.js):
|
|
46217
|
+
* `mountWhenClosed` builds it right away, `unmountWhenClosed` throws it away
|
|
46218
|
+
* once the collapse settles (so a closing animation still plays on real
|
|
46219
|
+
* content).
|
|
46220
|
+
*
|
|
46221
|
+
* The expansion animates the content's grid track (0fr <-> 1fr — rows for the
|
|
46222
|
+
* stacked layout, columns for `layout="column"`) rather than `height`/`width`:
|
|
46223
|
+
* the open size is "auto" (content-sized), which a length transition cannot
|
|
46224
|
+
* interpolate to, while a fr track can. The content is clipped only while the
|
|
46225
|
+
* track moves; once settled open it overflows normally again, so a popover or
|
|
46226
|
+
* focus ring inside is not cut at the edges.
|
|
46227
|
+
*/
|
|
46228
|
+
const css$J = /* css */`
|
|
46229
|
+
.navi_expandable {
|
|
46230
|
+
position: relative;
|
|
46231
|
+
display: flex;
|
|
46232
|
+
flex-shrink: 0;
|
|
46233
|
+
flex-direction: column;
|
|
46234
|
+
|
|
46235
|
+
> .navi_expandable_ui {
|
|
46236
|
+
display: flex;
|
|
46237
|
+
flex-shrink: 0;
|
|
46238
|
+
flex-direction: row;
|
|
46239
|
+
align-items: center;
|
|
46240
|
+
gap: 0.2em;
|
|
46241
|
+
cursor: pointer;
|
|
46242
|
+
user-select: none;
|
|
46243
|
+
|
|
46244
|
+
/* The whole row shows the focus, like a native <summary>, even though
|
|
46245
|
+
only the marker button actually holds it. */
|
|
46246
|
+
&:has(> .navi_expandable_toggle:focus-visible) {
|
|
46247
|
+
border-radius: 4px;
|
|
46248
|
+
outline: 2px solid AccentColor;
|
|
46249
|
+
outline-offset: 1px;
|
|
46250
|
+
}
|
|
46251
|
+
|
|
46252
|
+
> .navi_expandable_toggle {
|
|
46253
|
+
display: flex;
|
|
46254
|
+
padding: 0;
|
|
46255
|
+
align-items: center;
|
|
46256
|
+
color: inherit;
|
|
46257
|
+
font: inherit;
|
|
46258
|
+
background: none;
|
|
46259
|
+
border: none;
|
|
46260
|
+
cursor: pointer;
|
|
46261
|
+
|
|
46262
|
+
&:focus-visible {
|
|
46263
|
+
outline: none;
|
|
46264
|
+
}
|
|
46265
|
+
}
|
|
46266
|
+
|
|
46267
|
+
> .navi_expandable_ui_label {
|
|
46268
|
+
display: flex;
|
|
46269
|
+
flex: 1;
|
|
46270
|
+
align-items: center;
|
|
46271
|
+
gap: 0.2em;
|
|
46272
|
+
}
|
|
46273
|
+
}
|
|
46274
|
+
|
|
46275
|
+
> .navi_expandable_content_container {
|
|
46276
|
+
display: grid;
|
|
46277
|
+
grid-template-rows: 0fr;
|
|
46278
|
+
|
|
46279
|
+
> .navi_expandable_content {
|
|
46280
|
+
min-height: 0;
|
|
46281
|
+
overflow: hidden;
|
|
46282
|
+
}
|
|
46283
|
+
}
|
|
46284
|
+
&[aria-expanded="true"] > .navi_expandable_content_container {
|
|
46285
|
+
grid-template-rows: 1fr;
|
|
46286
|
+
}
|
|
46287
|
+
|
|
46288
|
+
/* The parts sit side by side: the UI part becomes a vertical strip and
|
|
46289
|
+
the content expands horizontally, on the columns track. */
|
|
46290
|
+
&[data-layout="column"] {
|
|
46291
|
+
flex-direction: row;
|
|
46292
|
+
|
|
46293
|
+
> .navi_expandable_ui {
|
|
46294
|
+
flex-direction: column;
|
|
46295
|
+
|
|
46296
|
+
> .navi_expandable_ui_label {
|
|
46297
|
+
flex-direction: column;
|
|
46298
|
+
}
|
|
46299
|
+
}
|
|
46300
|
+
> .navi_expandable_content_container {
|
|
46301
|
+
grid-template-columns: 0fr;
|
|
46302
|
+
grid-template-rows: none;
|
|
46303
|
+
|
|
46304
|
+
> .navi_expandable_content {
|
|
46305
|
+
min-width: 0;
|
|
46306
|
+
min-height: auto;
|
|
46307
|
+
}
|
|
46308
|
+
}
|
|
46309
|
+
&[aria-expanded="true"] > .navi_expandable_content_container {
|
|
46310
|
+
grid-template-columns: 1fr;
|
|
46311
|
+
grid-template-rows: none;
|
|
46312
|
+
}
|
|
46313
|
+
}
|
|
46314
|
+
|
|
46315
|
+
&[data-animation] > .navi_expandable_content_container {
|
|
46316
|
+
transition:
|
|
46317
|
+
grid-template-rows var(--navi-expandable-animation-duration, 0.3s) ease,
|
|
46318
|
+
grid-template-columns var(--navi-expandable-animation-duration, 0.3s)
|
|
46319
|
+
ease;
|
|
46320
|
+
}
|
|
46321
|
+
@media (prefers-reduced-motion: reduce) {
|
|
46322
|
+
&[data-animation] > .navi_expandable_content_container {
|
|
46323
|
+
transition: none;
|
|
46324
|
+
}
|
|
46325
|
+
}
|
|
46326
|
+
/* Settled open: stop clipping, so a popover, a focus ring or a dragged
|
|
46327
|
+
element inside the content can spill out — unless the content is given
|
|
46328
|
+
a max height, where the clipping IS the feature (it scrolls). */
|
|
46329
|
+
&[aria-expanded="true"][data-settled]:not([data-content-scrolls])
|
|
46330
|
+
> .navi_expandable_content_container
|
|
46331
|
+
> .navi_expandable_content {
|
|
46332
|
+
overflow: visible;
|
|
46333
|
+
}
|
|
46334
|
+
&[data-content-scrolls]
|
|
46335
|
+
> .navi_expandable_content_container
|
|
46336
|
+
> .navi_expandable_content {
|
|
46337
|
+
max-height: var(--navi-expandable-max-content-height);
|
|
46338
|
+
overflow-y: auto;
|
|
46339
|
+
}
|
|
46340
|
+
}
|
|
46341
|
+
`;
|
|
46342
|
+
const ExpandableContext = createContext(null);
|
|
46343
|
+
const useExpandableContext = partName => {
|
|
46344
|
+
const expandableContext = useContext(ExpandableContext);
|
|
46345
|
+
if (!expandableContext) {
|
|
46346
|
+
throw new Error(`<Expandable.${partName}> must be used inside <Expandable>`);
|
|
46347
|
+
}
|
|
46348
|
+
return expandableContext;
|
|
46349
|
+
};
|
|
46350
|
+
|
|
46351
|
+
/**
|
|
46352
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
46353
|
+
* ui?: import("ignore:preact").ComponentChildren | ((state: { open: boolean }) => import("ignore:preact").ComponentChildren),
|
|
46354
|
+
* open?: boolean,
|
|
46355
|
+
* defaultOpen?: boolean,
|
|
46356
|
+
* signal?: import("@preact/signals").Signal<boolean>,
|
|
46357
|
+
* onToggle?: (event: Event) => void,
|
|
46358
|
+
* action?: Function,
|
|
46359
|
+
* loading?: boolean,
|
|
46360
|
+
* animation?: boolean,
|
|
46361
|
+
* layout?: "row" | "column",
|
|
46362
|
+
* autoFocus?: boolean,
|
|
46363
|
+
* maxContentHeight?: string | number,
|
|
46364
|
+
* mountWhenClosed?: boolean,
|
|
46365
|
+
* unmountWhenClosed?: boolean,
|
|
46366
|
+
* arrowKeyShortcuts?: boolean,
|
|
46367
|
+
* openKeyShortcut?: string,
|
|
46368
|
+
* closeKeyShortcut?: string,
|
|
46369
|
+
* }>}
|
|
46370
|
+
* @param ui - Shorthand for the common shape: renders `<Expandable.UI>{ui}</Expandable.UI>`
|
|
46371
|
+
* above the content (children). Any markup is allowed (buttons, links,
|
|
46372
|
+
* fields inside it keep their own behavior and do not toggle the
|
|
46373
|
+
* expandable). A function receives `{ open }` to render differently per
|
|
46374
|
+
* state. For other orders/orientations, pass `<Expandable.UI>` and
|
|
46375
|
+
* `<Expandable.Content>` as children instead.
|
|
46376
|
+
* @param open - Drives the state from outside: the expandable opens/closes to
|
|
46377
|
+
* match every change of this prop, but user interaction can still toggle it
|
|
46378
|
+
* in between (same semantics as Dialog/Popover's own `open`).
|
|
46379
|
+
* @param defaultOpen - Uncontrolled, mount-only initial state.
|
|
46380
|
+
* @param signal - Two-way binding: the expandable follows the signal and
|
|
46381
|
+
* writes back into it whenever it toggles on its own. Excludes `open`.
|
|
46382
|
+
* @param onToggle - Listens the "toggle" event dispatched on the root (a
|
|
46383
|
+
* ToggleEvent with newState/oldState where supported). Fires on every actual
|
|
46384
|
+
* state change, never on mount.
|
|
46385
|
+
* @param action - Ran when the expandable opens, aborted when it closes.
|
|
46386
|
+
* Content children may then be a function `(data) => ui` or a branches
|
|
46387
|
+
* object (`{ loading, error, completed, ... }`) — see ActionRenderer.
|
|
46388
|
+
* @param loading - Shows the loading spinner on the marker regardless of
|
|
46389
|
+
* `action`'s own loading state.
|
|
46390
|
+
* @param animation - Off by default. `true` plays the expand/collapse track
|
|
46391
|
+
* transition; duration comes from `--navi-expandable-animation-duration`
|
|
46392
|
+
* (0.3s).
|
|
46393
|
+
* @param layout - `"row"` (default): the parts stack, the content expands
|
|
46394
|
+
* vertically. `"column"`: the parts sit side by side, the content expands
|
|
46395
|
+
* horizontally next to the UI part.
|
|
46396
|
+
* @param autoFocus - Off by default (the focus stays on the marker when
|
|
46397
|
+
* opening). `true` moves the focus into the content on open — the
|
|
46398
|
+
* `[autofocus]` element if any, the first focusable otherwise. Whatever the
|
|
46399
|
+
* setting, closing while the focus is inside the content hands it back to
|
|
46400
|
+
* the marker (it would otherwise be lost to the closed, inert content).
|
|
46401
|
+
* @param maxContentHeight - Caps the content height; taller content scrolls
|
|
46402
|
+
* inside the expandable instead of growing it.
|
|
46403
|
+
* @param mountWhenClosed - Builds the content right away instead of on first
|
|
46404
|
+
* expansion.
|
|
46405
|
+
* @param unmountWhenClosed - Throws the content away once the collapse
|
|
46406
|
+
* settles — after the closing animation, so it still plays on real content —
|
|
46407
|
+
* and rebuilds it from scratch on every expansion.
|
|
46408
|
+
*/
|
|
46409
|
+
const Expandable = props => {
|
|
46410
|
+
import.meta.css = [css$J, "@jsenv/navi/src/control/expandable/expandable.jsx"];
|
|
46411
|
+
const {
|
|
46412
|
+
ref,
|
|
46413
|
+
ui,
|
|
46414
|
+
open,
|
|
46415
|
+
defaultOpen,
|
|
46416
|
+
signal,
|
|
46417
|
+
action,
|
|
46418
|
+
loading,
|
|
46419
|
+
animation = false,
|
|
46420
|
+
layout,
|
|
46421
|
+
autoFocus,
|
|
46422
|
+
maxContentHeight,
|
|
46423
|
+
mountWhenClosed,
|
|
46424
|
+
unmountWhenClosed,
|
|
46425
|
+
arrowKeyShortcuts = true,
|
|
46426
|
+
openKeyShortcut = "ArrowRight",
|
|
46427
|
+
closeKeyShortcut = "ArrowLeft",
|
|
46428
|
+
children,
|
|
46429
|
+
...rest
|
|
46430
|
+
} = props;
|
|
46431
|
+
const defaultRef = useRef();
|
|
46432
|
+
const rootRef = ref || defaultRef;
|
|
46433
|
+
const uiRef = useRef();
|
|
46434
|
+
const toggleButtonRef = useRef();
|
|
46435
|
+
const contentContainerRef = useRef();
|
|
46436
|
+
const contentId = useId();
|
|
46437
|
+
const uiId = useId();
|
|
46438
|
+
// Reading .value during render is what subscribes the expandable to it.
|
|
46439
|
+
const openRequested = signal ? signal.value : open;
|
|
46440
|
+
const [opened, setOpened] = useState(() => Boolean(openRequested === undefined ? defaultOpen : openRequested));
|
|
46441
|
+
const openedRef = useRef(opened);
|
|
46442
|
+
openedRef.current = opened;
|
|
46443
|
+
const hasAction = Boolean(action);
|
|
46444
|
+
const effectiveAction = useAction(action);
|
|
46445
|
+
const {
|
|
46446
|
+
loading: actionLoading
|
|
46447
|
+
} = useActionStatus(effectiveAction);
|
|
46448
|
+
const [contentMounted, setContentMounted] = useState(() => Boolean(mountWhenClosed) || opened);
|
|
46449
|
+
// Same exclusion as popup_content_mount.js: content that must exist while
|
|
46450
|
+
// closed cannot also be thrown away on close.
|
|
46451
|
+
const effectiveUnmountWhenClosed = unmountWhenClosed && !mountWhenClosed;
|
|
46452
|
+
|
|
46453
|
+
// Fully open and no longer moving — what allows overflow to become visible
|
|
46454
|
+
// (see the CSS) and what unmountWhenClosed waits for before emptying.
|
|
46455
|
+
const [settled, setSettled] = useState(true);
|
|
46456
|
+
|
|
46457
|
+
// Read before the close touches the DOM: flipping the content to inert can
|
|
46458
|
+
// blur what it held, so by effect time the focus to hand back to the marker
|
|
46459
|
+
// could already be gone.
|
|
46460
|
+
const focusedBeforeCloseRef = useRef(null);
|
|
46461
|
+
// The pointer press that is about to toggle blurs the focused field before
|
|
46462
|
+
// the click ever fires (pressing a non-focusable row moves the focus to
|
|
46463
|
+
// body) — so what held the focus has to be remembered at pointerdown time.
|
|
46464
|
+
const focusedAtPointerDownRef = useRef(null);
|
|
46465
|
+
const onUIPointerDown = () => {
|
|
46466
|
+
focusedAtPointerDownRef.current = document.activeElement;
|
|
46467
|
+
};
|
|
46468
|
+
const toggleTo = nextOpen => {
|
|
46469
|
+
nextOpen = Boolean(nextOpen);
|
|
46470
|
+
if (nextOpen === openedRef.current) {
|
|
46471
|
+
return;
|
|
46472
|
+
}
|
|
46473
|
+
openedRef.current = nextOpen;
|
|
46474
|
+
if (nextOpen) {
|
|
46475
|
+
setContentMounted(true);
|
|
46476
|
+
} else {
|
|
46477
|
+
const activeElement = document.activeElement;
|
|
46478
|
+
focusedBeforeCloseRef.current = !activeElement || activeElement === document.body ? focusedAtPointerDownRef.current : activeElement;
|
|
46479
|
+
}
|
|
46480
|
+
focusedAtPointerDownRef.current = null;
|
|
46481
|
+
setOpened(nextOpen);
|
|
46482
|
+
// Flipped here, before the closing/opening commit, so effects of that very
|
|
46483
|
+
// commit already see the movement as started — unmountWhenClosed must not
|
|
46484
|
+
// read a stale "settled" and empty the content under a closing animation.
|
|
46485
|
+
setSettled(!animation);
|
|
46486
|
+
if (signal) {
|
|
46487
|
+
signal.value = nextOpen;
|
|
46488
|
+
}
|
|
46489
|
+
if (hasAction) {
|
|
46490
|
+
if (nextOpen) {
|
|
46491
|
+
effectiveAction.run();
|
|
46492
|
+
} else {
|
|
46493
|
+
effectiveAction.abort();
|
|
46494
|
+
}
|
|
46495
|
+
}
|
|
46496
|
+
};
|
|
46497
|
+
const findFirstFocusableInContent = () => {
|
|
46498
|
+
const contentContainer = contentContainerRef.current;
|
|
46499
|
+
if (!contentContainer) {
|
|
46500
|
+
return null;
|
|
46501
|
+
}
|
|
46502
|
+
const autofocusElement = contentContainer.querySelector("[autofocus]");
|
|
46503
|
+
if (autofocusElement) {
|
|
46504
|
+
return autofocusElement;
|
|
46505
|
+
}
|
|
46506
|
+
return findAfter(contentContainer, elementIsFocusable, {
|
|
46507
|
+
root: contentContainer
|
|
46508
|
+
});
|
|
46509
|
+
};
|
|
46510
|
+
|
|
46511
|
+
// Follow `open`/`signal` changes after mount (the initial value is already
|
|
46512
|
+
// in the state above). A self-initiated toggle that wrote the signal lands
|
|
46513
|
+
// here too and no-ops, since the state already matches.
|
|
46514
|
+
const isFirstOpenRequestedRunRef = useRef(true);
|
|
46515
|
+
useLayoutEffect(() => {
|
|
46516
|
+
if (isFirstOpenRequestedRunRef.current) {
|
|
46517
|
+
isFirstOpenRequestedRunRef.current = false;
|
|
46518
|
+
return;
|
|
46519
|
+
}
|
|
46520
|
+
if (openRequested === undefined) {
|
|
46521
|
+
return;
|
|
46522
|
+
}
|
|
46523
|
+
toggleTo(openRequested);
|
|
46524
|
+
}, [openRequested]);
|
|
46525
|
+
|
|
46526
|
+
// A state change: tell the world (the "toggle" event), move the focus, and
|
|
46527
|
+
// follow the transition to know when the movement is over. Skipped on mount —
|
|
46528
|
+
// nothing changed, so neither the event nor a transition exists (and a page
|
|
46529
|
+
// must not have its focus stolen by an expandable that was simply already
|
|
46530
|
+
// open).
|
|
46531
|
+
const isFirstOpenedRunRef = useRef(true);
|
|
46532
|
+
useLayoutEffect(() => {
|
|
46533
|
+
if (isFirstOpenedRunRef.current) {
|
|
46534
|
+
isFirstOpenedRunRef.current = false;
|
|
46535
|
+
return undefined;
|
|
46536
|
+
}
|
|
46537
|
+
const root = rootRef.current;
|
|
46538
|
+
root.dispatchEvent(createToggleEvent(opened));
|
|
46539
|
+
if (opened) {
|
|
46540
|
+
if (autoFocus) {
|
|
46541
|
+
const firstFocusableElement = findFirstFocusableInContent();
|
|
46542
|
+
if (firstFocusableElement) {
|
|
46543
|
+
firstFocusableElement.focus();
|
|
46544
|
+
}
|
|
46545
|
+
}
|
|
46546
|
+
} else {
|
|
46547
|
+
const focusedBeforeClose = focusedBeforeCloseRef.current;
|
|
46548
|
+
focusedBeforeCloseRef.current = null;
|
|
46549
|
+
if (focusedBeforeClose && contentContainerRef.current && contentContainerRef.current.contains(focusedBeforeClose)) {
|
|
46550
|
+
toggleButtonRef.current.focus();
|
|
46551
|
+
}
|
|
46552
|
+
}
|
|
46553
|
+
if (!animation) {
|
|
46554
|
+
return undefined;
|
|
46555
|
+
}
|
|
46556
|
+
const cancel = whenTransitionSettles(contentContainerRef.current, () => {
|
|
46557
|
+
setSettled(true);
|
|
46558
|
+
});
|
|
46559
|
+
return cancel;
|
|
46560
|
+
}, [opened]);
|
|
46561
|
+
useLayoutEffect(() => {
|
|
46562
|
+
if (settled && !opened && effectiveUnmountWhenClosed) {
|
|
46563
|
+
setContentMounted(false);
|
|
46564
|
+
}
|
|
46565
|
+
}, [settled, opened, effectiveUnmountWhenClosed]);
|
|
46566
|
+
useLayoutEffect(() => {
|
|
46567
|
+
if (mountWhenClosed) {
|
|
46568
|
+
setContentMounted(true);
|
|
46569
|
+
}
|
|
46570
|
+
}, [mountWhenClosed]);
|
|
46571
|
+
|
|
46572
|
+
// Mounted already open: the content is visible, its data is due.
|
|
46573
|
+
useEffect(() => {
|
|
46574
|
+
if (openedRef.current && hasAction) {
|
|
46575
|
+
effectiveAction.run();
|
|
46576
|
+
}
|
|
46577
|
+
}, []);
|
|
46578
|
+
const onRootKeyDown = keyboardEvent => {
|
|
46579
|
+
if (!arrowKeyShortcuts) {
|
|
46580
|
+
return;
|
|
46581
|
+
}
|
|
46582
|
+
// A nested expandable (deeper, so heard first) already answered this key.
|
|
46583
|
+
if (keyboardEvent.defaultPrevented) {
|
|
46584
|
+
return;
|
|
46585
|
+
}
|
|
46586
|
+
// Leave the key to whatever native use its target has for it (moving the
|
|
46587
|
+
// caret in a field, changing a slider) — only a press with nothing else
|
|
46588
|
+
// to do drives the expandable.
|
|
46589
|
+
const defaultAction = getKeyboardEventDefaultAction(keyboardEvent);
|
|
46590
|
+
if (defaultAction && defaultAction !== "scroll") {
|
|
46591
|
+
return;
|
|
46592
|
+
}
|
|
46593
|
+
const {
|
|
46594
|
+
key
|
|
46595
|
+
} = keyboardEvent;
|
|
46596
|
+
if (key === openKeyShortcut) {
|
|
46597
|
+
if (document.activeElement !== toggleButtonRef.current) {
|
|
46598
|
+
return;
|
|
46599
|
+
}
|
|
46600
|
+
if (!openedRef.current) {
|
|
46601
|
+
keyboardEvent.preventDefault();
|
|
46602
|
+
toggleTo(true);
|
|
46603
|
+
return;
|
|
46604
|
+
}
|
|
46605
|
+
const firstFocusableElementInContent = findFirstFocusableInContent();
|
|
46606
|
+
if (!firstFocusableElementInContent) {
|
|
46607
|
+
return;
|
|
46608
|
+
}
|
|
46609
|
+
keyboardEvent.preventDefault();
|
|
46610
|
+
firstFocusableElementInContent.focus();
|
|
46611
|
+
return;
|
|
46612
|
+
}
|
|
46613
|
+
if (key === closeKeyShortcut) {
|
|
46614
|
+
if (!openedRef.current) {
|
|
46615
|
+
return;
|
|
46616
|
+
}
|
|
46617
|
+
const toggleButton = toggleButtonRef.current;
|
|
46618
|
+
if (document.activeElement === toggleButton) {
|
|
46619
|
+
keyboardEvent.preventDefault();
|
|
46620
|
+
toggleTo(false);
|
|
46621
|
+
} else {
|
|
46622
|
+
keyboardEvent.preventDefault();
|
|
46623
|
+
toggleButton.focus();
|
|
46624
|
+
}
|
|
46625
|
+
}
|
|
46626
|
+
};
|
|
46627
|
+
const onUIClick = clickEvent => {
|
|
46628
|
+
// A navi control inside the UI part cancels the click it consumed (see
|
|
46629
|
+
// click_to_expand.js — the root's aria-expanded is what it finds).
|
|
46630
|
+
if (clickEvent.defaultPrevented) {
|
|
46631
|
+
return;
|
|
46632
|
+
}
|
|
46633
|
+
const {
|
|
46634
|
+
target
|
|
46635
|
+
} = clickEvent;
|
|
46636
|
+
if (target.nodeType === 1) {
|
|
46637
|
+
const interactiveElement = target.closest(UI_INTERACTIVE_SELECTOR);
|
|
46638
|
+
if (interactiveElement && interactiveElement !== toggleButtonRef.current && uiRef.current.contains(interactiveElement)) {
|
|
46639
|
+
return;
|
|
46640
|
+
}
|
|
46641
|
+
}
|
|
46642
|
+
toggleTo(!openedRef.current);
|
|
46643
|
+
};
|
|
46644
|
+
const expandableContextValue = {
|
|
46645
|
+
opened,
|
|
46646
|
+
loading: loading || hasAction && actionLoading,
|
|
46647
|
+
contentMounted,
|
|
46648
|
+
hasAction,
|
|
46649
|
+
effectiveAction,
|
|
46650
|
+
onUIClick,
|
|
46651
|
+
onUIPointerDown,
|
|
46652
|
+
uiRef,
|
|
46653
|
+
toggleButtonRef,
|
|
46654
|
+
contentContainerRef,
|
|
46655
|
+
contentId,
|
|
46656
|
+
uiId
|
|
46657
|
+
};
|
|
46658
|
+
|
|
46659
|
+
// Explicit parts win; the `ui` prop + children is the shorthand for the
|
|
46660
|
+
// common shape (UI above, content below).
|
|
46661
|
+
const childArray = toChildArray(children);
|
|
46662
|
+
const hasParts = childArray.some(child => child && (child.type === ExpandableUI || child.type === ExpandableContent));
|
|
46663
|
+
const body = hasParts ? children : jsxs(Fragment$1, {
|
|
46664
|
+
children: [jsx(ExpandableUI, {
|
|
46665
|
+
children: ui
|
|
46666
|
+
}), jsx(ExpandableContent, {
|
|
46667
|
+
children: children
|
|
46668
|
+
})]
|
|
46669
|
+
});
|
|
46670
|
+
return jsx(Box, {
|
|
46671
|
+
ref: rootRef,
|
|
46672
|
+
baseClassName: "navi_expandable",
|
|
46673
|
+
"aria-expanded": opened ? "true" : "false",
|
|
46674
|
+
"data-layout": layout === "column" ? "column" : undefined,
|
|
46675
|
+
"data-animation": animation ? "" : undefined,
|
|
46676
|
+
"data-settled": settled ? "" : undefined,
|
|
46677
|
+
"data-content-scrolls": maxContentHeight === undefined ? undefined : "",
|
|
46678
|
+
...rest,
|
|
46679
|
+
// The protocol every command target answers (see commands.js): a
|
|
46680
|
+
// `--navi-toggle`/`--navi-open`/`--navi-close` lands here as a
|
|
46681
|
+
// navi_command whose implementation dispatches the request events below.
|
|
46682
|
+
onnavi_command: e => {
|
|
46683
|
+
rest.onnavi_command?.(e);
|
|
46684
|
+
onNaviCommand(e);
|
|
46685
|
+
},
|
|
46686
|
+
onnavi_request_open: e => {
|
|
46687
|
+
rest.onnavi_request_open?.(e);
|
|
46688
|
+
toggleTo(true);
|
|
46689
|
+
},
|
|
46690
|
+
onnavi_request_close: e => {
|
|
46691
|
+
rest.onnavi_request_close?.(e);
|
|
46692
|
+
toggleTo(false);
|
|
46693
|
+
},
|
|
46694
|
+
onKeyDown: e => {
|
|
46695
|
+
rest.onKeyDown?.(e);
|
|
46696
|
+
onRootKeyDown(e);
|
|
46697
|
+
},
|
|
46698
|
+
style: maxContentHeight === undefined ? rest.style : {
|
|
46699
|
+
"--navi-expandable-max-content-height": stringifyStyle$1(maxContentHeight, "maxHeight"),
|
|
46700
|
+
...rest.style
|
|
46701
|
+
},
|
|
46702
|
+
children: jsx(ExpandableContext.Provider, {
|
|
46703
|
+
value: expandableContextValue,
|
|
46704
|
+
children: body
|
|
46705
|
+
})
|
|
46706
|
+
});
|
|
46707
|
+
};
|
|
46708
|
+
|
|
46709
|
+
/**
|
|
46710
|
+
* The always-visible part that reveals the content: the marker button plus
|
|
46711
|
+
* whatever it is given — any markup, a function of `{ open }` included. Its
|
|
46712
|
+
* position among the parts decides where the content goes (before the content:
|
|
46713
|
+
* content below/right; after it: content above/left).
|
|
46714
|
+
*
|
|
46715
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
46716
|
+
* children?: import("ignore:preact").ComponentChildren | ((state: { open: boolean }) => import("ignore:preact").ComponentChildren),
|
|
46717
|
+
* }>}
|
|
46718
|
+
*/
|
|
46719
|
+
const ExpandableUI = ({
|
|
46720
|
+
children,
|
|
46721
|
+
...rest
|
|
46722
|
+
}) => {
|
|
46723
|
+
const {
|
|
46724
|
+
opened,
|
|
46725
|
+
loading,
|
|
46726
|
+
onUIClick,
|
|
46727
|
+
onUIPointerDown,
|
|
46728
|
+
uiRef,
|
|
46729
|
+
toggleButtonRef,
|
|
46730
|
+
contentId,
|
|
46731
|
+
uiId
|
|
46732
|
+
} = useExpandableContext("UI");
|
|
46733
|
+
return jsxs("div", {
|
|
46734
|
+
ref: uiRef,
|
|
46735
|
+
className: "navi_expandable_ui",
|
|
46736
|
+
onClick: onUIClick,
|
|
46737
|
+
onPointerDown: onUIPointerDown,
|
|
46738
|
+
...rest,
|
|
46739
|
+
children: [jsx("button", {
|
|
46740
|
+
ref: toggleButtonRef,
|
|
46741
|
+
type: "button",
|
|
46742
|
+
className: "navi_expandable_toggle",
|
|
46743
|
+
"aria-expanded": opened,
|
|
46744
|
+
"aria-controls": contentId,
|
|
46745
|
+
"aria-labelledby": uiId,
|
|
46746
|
+
children: jsx(SummaryMarker, {
|
|
46747
|
+
open: opened,
|
|
46748
|
+
loading: loading
|
|
46749
|
+
})
|
|
46750
|
+
}), jsx("div", {
|
|
46751
|
+
id: uiId,
|
|
46752
|
+
className: "navi_expandable_ui_label",
|
|
46753
|
+
children: typeof children === "function" ? children({
|
|
46754
|
+
open: opened
|
|
46755
|
+
}) : children
|
|
46756
|
+
})]
|
|
46757
|
+
});
|
|
46758
|
+
};
|
|
46759
|
+
|
|
46760
|
+
/**
|
|
46761
|
+
* The revealed part. With an `action` on the Expandable, children may be a
|
|
46762
|
+
* function `(data) => ui` or a branches object — see ActionRenderer.
|
|
46763
|
+
*
|
|
46764
|
+
* @type {import("ignore:preact").FunctionComponent<{}>}
|
|
46765
|
+
*/
|
|
46766
|
+
const ExpandableContent = ({
|
|
46767
|
+
children,
|
|
46768
|
+
...rest
|
|
46769
|
+
}) => {
|
|
46770
|
+
const {
|
|
46771
|
+
opened,
|
|
46772
|
+
contentMounted,
|
|
46773
|
+
hasAction,
|
|
46774
|
+
effectiveAction,
|
|
46775
|
+
contentContainerRef,
|
|
46776
|
+
contentId
|
|
46777
|
+
} = useExpandableContext("Content");
|
|
46778
|
+
let content = children;
|
|
46779
|
+
if (hasAction) {
|
|
46780
|
+
content = jsx(ActionRenderer, {
|
|
46781
|
+
action: effectiveAction,
|
|
46782
|
+
children: children
|
|
46783
|
+
});
|
|
46784
|
+
}
|
|
46785
|
+
return jsx("div", {
|
|
46786
|
+
ref: contentContainerRef,
|
|
46787
|
+
id: contentId,
|
|
46788
|
+
className: "navi_expandable_content_container",
|
|
46789
|
+
inert: opened ? undefined : true,
|
|
46790
|
+
...rest,
|
|
46791
|
+
children: jsx("div", {
|
|
46792
|
+
className: "navi_expandable_content",
|
|
46793
|
+
children: contentMounted ? content : null
|
|
46794
|
+
})
|
|
46795
|
+
});
|
|
46796
|
+
};
|
|
46797
|
+
Expandable.UI = ExpandableUI;
|
|
46798
|
+
Expandable.Content = ExpandableContent;
|
|
46799
|
+
|
|
46800
|
+
// What a click inside the UI part must not toggle: it was aimed at the
|
|
46801
|
+
// control, not at the row. The marker button is the one exception, excluded at
|
|
46802
|
+
// the call site.
|
|
46803
|
+
const UI_INTERACTIVE_SELECTOR = ["a[href]", "button", "input", "select", "textarea", "label", "[role='button']", "[contenteditable='']", "[contenteditable='true']", "audio[controls]", "video[controls]"].join(", ");
|
|
46804
|
+
const createToggleEvent = open => {
|
|
46805
|
+
const newState = open ? "open" : "closed";
|
|
46806
|
+
const oldState = open ? "closed" : "open";
|
|
46807
|
+
if (typeof window.ToggleEvent === "function") {
|
|
46808
|
+
return new window.ToggleEvent("toggle", {
|
|
46809
|
+
newState,
|
|
46810
|
+
oldState
|
|
46811
|
+
});
|
|
46812
|
+
}
|
|
46813
|
+
const toggleEvent = new CustomEvent("toggle");
|
|
46814
|
+
toggleEvent.newState = newState;
|
|
46815
|
+
toggleEvent.oldState = oldState;
|
|
46816
|
+
return toggleEvent;
|
|
46817
|
+
};
|
|
46818
|
+
|
|
46022
46819
|
const DEFAULT_VALIDITY_STATE = { valid: true };
|
|
46023
46820
|
const useConstraintValidityState = (ref) => {
|
|
46024
46821
|
const checkValue = () => {
|
|
@@ -54163,6 +54960,9 @@ const css$A = /* css */`
|
|
|
54163
54960
|
* to `--dialog-maxmax-height` (`data-expand-y`).
|
|
54164
54961
|
* @param {boolean} [props.scrollCapture] - Forwarded as-is.
|
|
54165
54962
|
* @param {boolean} [props.open] - Forwarded as-is (controlled).
|
|
54963
|
+
* @param {import("@preact/signals").Signal<boolean>} [props.signal] -
|
|
54964
|
+
* Forwarded as-is: one binding to both drive the popup's open state and
|
|
54965
|
+
* know where it is (see `Dialog`/`Popover`'s own `signal`).
|
|
54166
54966
|
* @param {boolean} [props.defaultOpen] - Forwarded as-is (uncontrolled,
|
|
54167
54967
|
* mount-only).
|
|
54168
54968
|
* @param {(event: Event) => void} [props.onClose] - Forwarded as-is.
|
|
@@ -68230,6 +69030,10 @@ const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
|
|
|
68230
69030
|
* @param {string} [value] The time shown, as "HH:MM".
|
|
68231
69031
|
* @param {number} [minuteStep=1] How many minutes apart the values on the
|
|
68232
69032
|
* minute wheel are — 15 for quarters of an hour.
|
|
69033
|
+
* @param {{min?: number, max?: number}|number[]} [hours] Which hours the wheel
|
|
69034
|
+
* offers: `{ min: 7, max: 21 }` for a day that starts and ends somewhere, or
|
|
69035
|
+
* the list itself. All 24 by default. Rows nobody will ever land on are rows
|
|
69036
|
+
* in the way.
|
|
68233
69037
|
* @param {boolean} [loop=true] The wheels go round: 23h then 0h, 59 minutes
|
|
68234
69038
|
* then 0. What a clock does. Say `loop={false}` for two ends one cannot turn
|
|
68235
69039
|
* past.
|
|
@@ -68246,6 +69050,7 @@ const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
|
|
|
68246
69050
|
*/
|
|
68247
69051
|
const TimeWheel = ({
|
|
68248
69052
|
minuteStep = 1,
|
|
69053
|
+
hours,
|
|
68249
69054
|
loop = true,
|
|
68250
69055
|
placeholder,
|
|
68251
69056
|
separator = naviI18n("time.hour_separator"),
|
|
@@ -68264,13 +69069,15 @@ const TimeWheel = ({
|
|
|
68264
69069
|
}
|
|
68265
69070
|
return minuteList;
|
|
68266
69071
|
}, [minuteStep]);
|
|
69072
|
+
const hourList = useMemo(() => resolveHourList(hours), [hours ? hours.min : undefined, hours ? hours.max : undefined, hours]);
|
|
68267
69073
|
const {
|
|
68268
|
-
aggregateChildStates
|
|
68269
|
-
|
|
69074
|
+
aggregateChildStates,
|
|
69075
|
+
distributeChildUIState
|
|
69076
|
+
} = useAnswered(placeholder, rest, aggregateTime, distributeTime);
|
|
68270
69077
|
const placeholderParts = parseTimeParts(placeholder);
|
|
68271
69078
|
return jsxs(WheelGroup, {
|
|
68272
69079
|
aggregateChildStates: aggregateChildStates,
|
|
68273
|
-
distributeChildUIState:
|
|
69080
|
+
distributeChildUIState: distributeChildUIState,
|
|
68274
69081
|
...rest,
|
|
68275
69082
|
children: [jsx(Wheel, {
|
|
68276
69083
|
name: "hour",
|
|
@@ -68280,7 +69087,7 @@ const TimeWheel = ({
|
|
|
68280
69087
|
"aria-label": hourLabel,
|
|
68281
69088
|
defaultValue: placeholderParts ? placeholderParts.hour : undefined,
|
|
68282
69089
|
...wheelProps,
|
|
68283
|
-
children:
|
|
69090
|
+
children: hourList.map(hour => jsx(Wheel.Item, {
|
|
68284
69091
|
value: hour,
|
|
68285
69092
|
paddingX: "s",
|
|
68286
69093
|
children: padTwo(hour)
|
|
@@ -68327,6 +69134,8 @@ const TimeWheel = ({
|
|
|
68327
69134
|
* `null` for neither.
|
|
68328
69135
|
* @param {number} [minuteStep=1] How many minutes apart the values on both
|
|
68329
69136
|
* minute wheels are.
|
|
69137
|
+
* @param {{min?: number, max?: number}|number[]} [hours] Which hours both
|
|
69138
|
+
* wheels offer — see `TimeWheel`.
|
|
68330
69139
|
* @param {number} [minDuration=0] How long the span must last at least, in
|
|
68331
69140
|
* minutes. Zero by default: a span of no length is a span all the same, only
|
|
68332
69141
|
* one that goes backwards is not. It is what the bounds keep between them as
|
|
@@ -68344,6 +69153,7 @@ const TimeWheel = ({
|
|
|
68344
69153
|
*/
|
|
68345
69154
|
const TimeRangeWheel = ({
|
|
68346
69155
|
minuteStep = 1,
|
|
69156
|
+
hours,
|
|
68347
69157
|
minDuration = 0,
|
|
68348
69158
|
loop = true,
|
|
68349
69159
|
placeholder,
|
|
@@ -68362,8 +69172,9 @@ const TimeRangeWheel = ({
|
|
|
68362
69172
|
// answer too, left where the placeholder put it.
|
|
68363
69173
|
const {
|
|
68364
69174
|
answeredRef,
|
|
68365
|
-
aggregateChildStates
|
|
68366
|
-
|
|
69175
|
+
aggregateChildStates,
|
|
69176
|
+
distributeChildUIState
|
|
69177
|
+
} = useAnswered(placeholder, rest, aggregateSpan, distributeSpan);
|
|
68367
69178
|
|
|
68368
69179
|
// What the pair does while it is being turned: the bound that just moved is
|
|
68369
69180
|
// the one the user is holding, so it stays where it was put and the OTHER one
|
|
@@ -68406,6 +69217,7 @@ const TimeRangeWheel = ({
|
|
|
68406
69217
|
spacing: "s",
|
|
68407
69218
|
size: size,
|
|
68408
69219
|
aggregateChildStates: aggregateChildStates,
|
|
69220
|
+
distributeChildUIState: distributeChildUIState,
|
|
68409
69221
|
...rest,
|
|
68410
69222
|
children: jsxs(AnsweredContext.Provider, {
|
|
68411
69223
|
value: answeredRef,
|
|
@@ -68417,6 +69229,7 @@ const TimeRangeWheel = ({
|
|
|
68417
69229
|
ref: startRef,
|
|
68418
69230
|
name: "start",
|
|
68419
69231
|
minuteStep: minuteStep,
|
|
69232
|
+
hours: hours,
|
|
68420
69233
|
loop: loop,
|
|
68421
69234
|
size: size,
|
|
68422
69235
|
placeholder: placeholder ? placeholder.start : undefined,
|
|
@@ -68430,6 +69243,7 @@ const TimeRangeWheel = ({
|
|
|
68430
69243
|
ref: endRef,
|
|
68431
69244
|
name: "end",
|
|
68432
69245
|
minuteStep: minuteStep,
|
|
69246
|
+
hours: hours,
|
|
68433
69247
|
loop: loop,
|
|
68434
69248
|
size: size,
|
|
68435
69249
|
placeholder: placeholder ? placeholder.end : undefined,
|
|
@@ -68471,7 +69285,7 @@ const TimeRangeWheel = ({
|
|
|
68471
69285
|
* compare its own placeholder against.
|
|
68472
69286
|
*/
|
|
68473
69287
|
const AnsweredContext = createContext(null);
|
|
68474
|
-
const useAnswered = (placeholder, props, aggregateWhenAnswered) => {
|
|
69288
|
+
const useAnswered = (placeholder, props, aggregateWhenAnswered, distributeWhenAnswered) => {
|
|
68475
69289
|
const answeredFromPair = useContext(AnsweredContext);
|
|
68476
69290
|
const ownAnsweredRef = useRef(false);
|
|
68477
69291
|
const answeredRef = answeredFromPair || ownAnsweredRef;
|
|
@@ -68483,31 +69297,81 @@ const useAnswered = (placeholder, props, aggregateWhenAnswered) => {
|
|
|
68483
69297
|
const gates = !answeredFromPair;
|
|
68484
69298
|
const placeholderRef = useRef(placeholder);
|
|
68485
69299
|
placeholderRef.current = placeholder;
|
|
68486
|
-
const
|
|
68487
|
-
if (!
|
|
68488
|
-
|
|
68489
|
-
|
|
68490
|
-
|
|
69300
|
+
const scopeRef = useRef(null);
|
|
69301
|
+
if (!scopeRef.current) {
|
|
69302
|
+
scopeRef.current = {
|
|
69303
|
+
aggregateChildStates: children => {
|
|
69304
|
+
const aggregated = aggregateWhenAnswered(children);
|
|
69305
|
+
if (answeredRef.current) {
|
|
69306
|
+
return aggregated;
|
|
69307
|
+
}
|
|
69308
|
+
if (compareTwoJsValues(aggregated, placeholderRef.current)) {
|
|
69309
|
+
return undefined;
|
|
69310
|
+
}
|
|
69311
|
+
// It moved: from here on this is an answer, and stays one even when it
|
|
69312
|
+
// is turned back onto the placeholder — somebody chose that time.
|
|
69313
|
+
answeredRef.current = true;
|
|
68491
69314
|
return aggregated;
|
|
69315
|
+
},
|
|
69316
|
+
distributeChildUIState: (groupState, child) => {
|
|
69317
|
+
if (placeholderRef.current && holdsNothing(groupState)) {
|
|
69318
|
+
// Being told there is no value is not a finger bringing a wheel back:
|
|
69319
|
+
// it is a clear, or the app writing `undefined`. The wheels go back to
|
|
69320
|
+
// showing the placeholder, and the pair is unanswered again — without
|
|
69321
|
+
// this the wheels would empty (a wheel has no blank row) and what they
|
|
69322
|
+
// still showed would climb straight back up as an answer.
|
|
69323
|
+
answeredRef.current = false;
|
|
69324
|
+
return distributeWhenAnswered(placeholderRef.current, child);
|
|
69325
|
+
}
|
|
69326
|
+
return distributeWhenAnswered(groupState, child);
|
|
68492
69327
|
}
|
|
68493
|
-
if (compareTwoJsValues(aggregated, placeholderRef.current)) {
|
|
68494
|
-
return undefined;
|
|
68495
|
-
}
|
|
68496
|
-
// It moved: from here on this is an answer, and stays one even when it is
|
|
68497
|
-
// turned back onto the placeholder — somebody chose that time.
|
|
68498
|
-
answeredRef.current = true;
|
|
68499
|
-
return aggregated;
|
|
68500
69328
|
};
|
|
68501
69329
|
}
|
|
68502
69330
|
return {
|
|
68503
69331
|
answeredRef,
|
|
68504
|
-
aggregateChildStates: gates ?
|
|
69332
|
+
aggregateChildStates: gates ? scopeRef.current.aggregateChildStates : aggregateWhenAnswered,
|
|
69333
|
+
distributeChildUIState: gates ? scopeRef.current.distributeChildUIState : distributeWhenAnswered
|
|
68505
69334
|
};
|
|
68506
69335
|
};
|
|
69336
|
+
|
|
69337
|
+
// Nothing at all: no value, an empty shape, or a shape whose every part is
|
|
69338
|
+
// itself nothing — which is what a cleared span looks like on the way down.
|
|
69339
|
+
const holdsNothing = value => {
|
|
69340
|
+
if (value === undefined || value === null || value === "") {
|
|
69341
|
+
return true;
|
|
69342
|
+
}
|
|
69343
|
+
if (typeof value !== "object") {
|
|
69344
|
+
return false;
|
|
69345
|
+
}
|
|
69346
|
+
return Object.values(value).every(holdsNothing);
|
|
69347
|
+
};
|
|
68507
69348
|
const isAnswerGivenByProps = props => props.value !== undefined || props.defaultValue !== undefined || props.signal && props.signal.value !== undefined;
|
|
68508
|
-
|
|
69349
|
+
|
|
69350
|
+
// Which hours the wheel offers: all of them, a slice of the day, or a list
|
|
69351
|
+
// written by the caller. An app whose day ends at 21h has nothing to say about
|
|
69352
|
+
// 22h and 23h, and two rows nobody will ever land on are two rows in the way.
|
|
69353
|
+
const ALL_HOURS = Array.from({
|
|
68509
69354
|
length: HOUR_COUNT
|
|
68510
69355
|
}, (_, hour) => hour);
|
|
69356
|
+
const resolveHourList = hours => {
|
|
69357
|
+
if (hours === undefined) {
|
|
69358
|
+
return ALL_HOURS;
|
|
69359
|
+
}
|
|
69360
|
+
if (Array.isArray(hours)) {
|
|
69361
|
+
return hours;
|
|
69362
|
+
}
|
|
69363
|
+
const {
|
|
69364
|
+
min = 0,
|
|
69365
|
+
max = HOUR_COUNT - 1
|
|
69366
|
+
} = hours;
|
|
69367
|
+
const hourList = [];
|
|
69368
|
+
let hour = min;
|
|
69369
|
+
while (hour <= max) {
|
|
69370
|
+
hourList.push(hour);
|
|
69371
|
+
hour += 1;
|
|
69372
|
+
}
|
|
69373
|
+
return hourList;
|
|
69374
|
+
};
|
|
68511
69375
|
const padTwo = value => String(value).padStart(2, "0");
|
|
68512
69376
|
|
|
68513
69377
|
// The two times as one span, { start, end } — the shape a pair carries.
|
|
@@ -68521,6 +69385,14 @@ const aggregateSpan = childUIStateControllers => {
|
|
|
68521
69385
|
return span;
|
|
68522
69386
|
};
|
|
68523
69387
|
|
|
69388
|
+
// The way back for a span: each time takes its own side.
|
|
69389
|
+
const distributeSpan = (groupState, childUIStateController) => {
|
|
69390
|
+
if (!groupState) {
|
|
69391
|
+
return undefined;
|
|
69392
|
+
}
|
|
69393
|
+
return groupState[childUIStateController.name];
|
|
69394
|
+
};
|
|
69395
|
+
|
|
68524
69396
|
// The two wheels as one value, "HH:MM".
|
|
68525
69397
|
const aggregateTime = childUIStateControllers => {
|
|
68526
69398
|
let hour = "";
|
|
@@ -74247,6 +75119,12 @@ const css = /* css */`
|
|
|
74247
75119
|
* @param {object} props
|
|
74248
75120
|
* @param {boolean} [props.open] - Controlled open state, forwarded as-is to
|
|
74249
75121
|
* `Popup`'s own `open`.
|
|
75122
|
+
* @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
|
|
75123
|
+
* state said the way every navi control says it: the panel opens and closes
|
|
75124
|
+
* to match the signal, and writes into it whenever it opens or closes on its
|
|
75125
|
+
* own (Escape, swipe, a --navi-close command) — one binding to both drive
|
|
75126
|
+
* the panel and know where it is. Forwarded as-is to `Popup`; excludes
|
|
75127
|
+
* `open` (see `Dialog`/`Popover`'s own `signal`).
|
|
74250
75128
|
* @param {boolean} [props.defaultOpen] - Uncontrolled, mount-only initial
|
|
74251
75129
|
* open state, forwarded as-is to `Popup`. Neither this nor `open` is
|
|
74252
75130
|
* required at all for a purely command-driven panel (an `id` plus a
|
|
@@ -74313,6 +75191,7 @@ const css = /* css */`
|
|
|
74313
75191
|
*/
|
|
74314
75192
|
const SidePanel = ({
|
|
74315
75193
|
open,
|
|
75194
|
+
signal,
|
|
74316
75195
|
defaultOpen,
|
|
74317
75196
|
onClose,
|
|
74318
75197
|
children,
|
|
@@ -74334,6 +75213,7 @@ const SidePanel = ({
|
|
|
74334
75213
|
return jsx(Popup, {
|
|
74335
75214
|
mode: mode,
|
|
74336
75215
|
open: open,
|
|
75216
|
+
signal: signal,
|
|
74337
75217
|
defaultOpen: defaultOpen,
|
|
74338
75218
|
onClose: onClose,
|
|
74339
75219
|
layer: layer,
|
|
@@ -74572,5 +75452,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
74572
75452
|
})
|
|
74573
75453
|
});
|
|
74574
75454
|
|
|
74575
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
|
|
75455
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
|
|
74576
75456
|
//# sourceMappingURL=jsenv_navi.js.map
|