@jsenv/navi 0.29.41 → 0.29.43
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 +438 -205
- package/dist/jsenv_navi.js.map +34 -19
- package/dist/jsenv_navi_side_effects.js +5 -0
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/AI_INSTRUCTIONS.md +6 -0
- package/docs/control_value.md +2 -0
- package/docs/form_changed.md +127 -0
- package/docs/interactions.md +27 -0
- package/docs/scroll.md +54 -4
- package/docs/z_index.md +41 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -51,7 +51,15 @@ const css$10 = /* css */`
|
|
|
51
51
|
/* Kept stuck while something scrolls under it: a list header, the head
|
|
52
52
|
and foot of a side panel, a table's sticky cells. Above raised
|
|
53
53
|
controls — a control scrolling past must go under the header that
|
|
54
|
-
pins the column it belongs to, never over it.
|
|
54
|
+
pins the column it belongs to, never over it.
|
|
55
|
+
|
|
56
|
+
"While stuck" is the whole condition, and a sticky element cannot read
|
|
57
|
+
its own stuck state in CSS: List marks its parts with a navi-stuck
|
|
58
|
+
attribute and applies this band only there (see --list-*-z-index in
|
|
59
|
+
list.jsx). A
|
|
60
|
+
sticky part at rest is a block in the flow with nothing passing under
|
|
61
|
+
it; giving it this band anyway is what slices whatever a neighbouring
|
|
62
|
+
row lets out of its box. */
|
|
55
63
|
--navi-z-index-sticky: 10;
|
|
56
64
|
|
|
57
65
|
/* Pinned to the viewport, over the whole page: FixedBar. A decade of its
|
|
@@ -22983,6 +22991,9 @@ const useUIStateController = (
|
|
|
22983
22991
|
// resolveCommandValue in commands.js.
|
|
22984
22992
|
ownUIStateSignal,
|
|
22985
22993
|
value: controlInfo.value,
|
|
22994
|
+
// The suggestion this control started on — what tells a field showing
|
|
22995
|
+
// its default from one carrying an answer (see isUIStateHeld).
|
|
22996
|
+
defaultValue: controlInfo.defaultValue,
|
|
22986
22997
|
|
|
22987
22998
|
facadeChild: null,
|
|
22988
22999
|
getManagedControls: () => {
|
|
@@ -23401,9 +23412,16 @@ const useUIStateController = (
|
|
|
23401
23412
|
controller.id = props.id; // never supposed to change, not supported for now
|
|
23402
23413
|
controller.name = props.name;
|
|
23403
23414
|
controller.parentUIStateController = parentUIStateController;
|
|
23404
|
-
const {
|
|
23405
|
-
|
|
23415
|
+
const {
|
|
23416
|
+
value,
|
|
23417
|
+
defaultValue,
|
|
23418
|
+
hasStateProp,
|
|
23419
|
+
state,
|
|
23420
|
+
stateInitial,
|
|
23421
|
+
stateFromSignal,
|
|
23422
|
+
} = controlInfo;
|
|
23406
23423
|
controller.value = value;
|
|
23424
|
+
controller.defaultValue = defaultValue;
|
|
23407
23425
|
if (hasStateProp) {
|
|
23408
23426
|
controller.hasStateProp = true;
|
|
23409
23427
|
const currentState = controller.state;
|
|
@@ -24334,8 +24352,10 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
24334
24352
|
const childType = child.controlType;
|
|
24335
24353
|
if (firstChildControllerRef.current) {
|
|
24336
24354
|
console.warn(
|
|
24337
|
-
`[
|
|
24338
|
-
`
|
|
24355
|
+
`[navi] a second control ("${childType}"${child.name ? ` name="${child.name}"` : ""}) registered in the ${describePicker(props)} popup. ` +
|
|
24356
|
+
`A picker talks to ONE control: the first one receives the picker's whole value and is the only one read back, ` +
|
|
24357
|
+
`so this one is neither filled nor collected. ` +
|
|
24358
|
+
`A popup holding several values needs one group around them — wrap them in a <ControlGroup>, name each control inside it, and give the picker type="form".`,
|
|
24339
24359
|
child,
|
|
24340
24360
|
);
|
|
24341
24361
|
} else {
|
|
@@ -24463,6 +24483,9 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
24463
24483
|
return scope.controller;
|
|
24464
24484
|
};
|
|
24465
24485
|
|
|
24486
|
+
const describePicker = (props) =>
|
|
24487
|
+
`<Picker${props.name ? ` name="${props.name}"` : ""}${props.type ? ` type="${props.type}"` : ""}>`;
|
|
24488
|
+
|
|
24466
24489
|
/**
|
|
24467
24490
|
* Returns true when `e` should trigger parent notification (child → parent bubbling).
|
|
24468
24491
|
*
|
|
@@ -24612,12 +24635,19 @@ const ControlgroupChildrenWrapper = ({
|
|
|
24612
24635
|
*/
|
|
24613
24636
|
const useControlProps = (props, {
|
|
24614
24637
|
controlType,
|
|
24615
|
-
allowNameless,
|
|
24638
|
+
allowNameless: allowNamelessByDefault,
|
|
24616
24639
|
persists,
|
|
24617
24640
|
uiActionInternal
|
|
24618
24641
|
}) => {
|
|
24619
24642
|
const debugUIState = useDebugUIState();
|
|
24620
24643
|
const debugAction = useDebugAction();
|
|
24644
|
+
|
|
24645
|
+
// A control that is not a field: it opens something, it goes somewhere, and
|
|
24646
|
+
// the group around it must expect no value from it — no name, and no warning
|
|
24647
|
+
// about the missing name. Buttons and links say so from inside navi; the prop
|
|
24648
|
+
// is how a control used as a door says the same thing from the outside.
|
|
24649
|
+
const allowNameless = props.allowNameless ?? allowNamelessByDefault;
|
|
24650
|
+
delete props.allowNameless;
|
|
24621
24651
|
const idDefault = useId();
|
|
24622
24652
|
const controlId = useContext(ControlIdContext);
|
|
24623
24653
|
props.id = props.id || controlId || idDefault;
|
|
@@ -25426,6 +25456,18 @@ const createControlInfo = (props, {
|
|
|
25426
25456
|
// getDefaultEventReactionDefinitions.
|
|
25427
25457
|
readOnlySupported = controlType === "picker" && INPUT_TYPE_SUPPORTING_READONLY_SET.has(typeProp);
|
|
25428
25458
|
}
|
|
25459
|
+
|
|
25460
|
+
// The suggestion the control starts on, as opposed to what it holds — what a
|
|
25461
|
+
// reset goes back to, and what tells a field left on its default from one
|
|
25462
|
+
// carrying an answer (see isUIStateHeld in held_ui_state.js).
|
|
25463
|
+
let defaultValue;
|
|
25464
|
+
if (!hasStateProp) {
|
|
25465
|
+
if (signalHoldsChecked) {
|
|
25466
|
+
defaultValue = props.defaultChecked ? value : undefined;
|
|
25467
|
+
} else if (Object.hasOwn(props, "defaultValue")) {
|
|
25468
|
+
defaultValue = props.defaultValue;
|
|
25469
|
+
}
|
|
25470
|
+
}
|
|
25429
25471
|
return {
|
|
25430
25472
|
controlType,
|
|
25431
25473
|
statePropName,
|
|
@@ -25434,6 +25476,7 @@ const createControlInfo = (props, {
|
|
|
25434
25476
|
stateInitial,
|
|
25435
25477
|
state: stateInitial,
|
|
25436
25478
|
value,
|
|
25479
|
+
defaultValue,
|
|
25437
25480
|
signal,
|
|
25438
25481
|
signalHoldsChecked,
|
|
25439
25482
|
stateFromSignal,
|
|
@@ -28339,6 +28382,20 @@ const css$V = /* css */`
|
|
|
28339
28382
|
backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
|
|
28340
28383
|
}
|
|
28341
28384
|
|
|
28385
|
+
/* backdropAppearance, keyed off the originating element (a
|
|
28386
|
+
pseudo-element carries no attributes of its own — same reasoning as
|
|
28387
|
+
the capture rule just above). After the rules it overrides: same
|
|
28388
|
+
specificity, so order is what decides. showModal() still makes the
|
|
28389
|
+
page inert either way — only the paint goes away. */
|
|
28390
|
+
&[data-backdrop-appearance="discrete"]::backdrop {
|
|
28391
|
+
background: var(--navi-backdrop-discrete-background);
|
|
28392
|
+
backdrop-filter: none;
|
|
28393
|
+
}
|
|
28394
|
+
&[data-backdrop-appearance="none"]::backdrop {
|
|
28395
|
+
background: transparent;
|
|
28396
|
+
backdrop-filter: none;
|
|
28397
|
+
}
|
|
28398
|
+
|
|
28342
28399
|
/* Nested under &[navi-animation] (not the other way around) so every
|
|
28343
28400
|
attribute selector compiles *before* ::backdrop, not after — a
|
|
28344
28401
|
pseudo-element can't be qualified by an attribute of its own
|
|
@@ -28458,6 +28515,18 @@ const css$V = /* css */`
|
|
|
28458
28515
|
backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
|
|
28459
28516
|
}
|
|
28460
28517
|
|
|
28518
|
+
/* Same override as the via-attribute renderer's own ::backdrop rules
|
|
28519
|
+
above, on the real element this renderer uses instead — see them for
|
|
28520
|
+
the specificity/ordering reasoning. */
|
|
28521
|
+
&[data-backdrop-appearance="discrete"] {
|
|
28522
|
+
background: var(--navi-backdrop-discrete-background);
|
|
28523
|
+
backdrop-filter: none;
|
|
28524
|
+
}
|
|
28525
|
+
&[data-backdrop-appearance="none"] {
|
|
28526
|
+
background: transparent;
|
|
28527
|
+
backdrop-filter: none;
|
|
28528
|
+
}
|
|
28529
|
+
|
|
28461
28530
|
&[navi-animation] {
|
|
28462
28531
|
opacity: 1;
|
|
28463
28532
|
transition-property: display, opacity;
|
|
@@ -28528,6 +28597,13 @@ const css$V = /* css */`
|
|
|
28528
28597
|
* both just absorb the click without closing (visually dimmed backdrop vs.
|
|
28529
28598
|
* not) — a dialog is always modal one way or another, so there's always
|
|
28530
28599
|
* at least a click-absorbing backdrop regardless of this prop.
|
|
28600
|
+
* @param {"auto"|"discrete"|"none"} [props.backdropAppearance="auto"] - How
|
|
28601
|
+
* visible the backdrop is, independently of what it does. `"auto"`: the
|
|
28602
|
+
* paint `pointerInteractionOutsideEffect` implies (dimmed for
|
|
28603
|
+
* `"close"`/`"cancel"`, blurred glass for `"capture"`). `"discrete"`: a
|
|
28604
|
+
* barely-there dim. `"none"`: fully transparent. The dialog stays modal
|
|
28605
|
+
* either way — this only changes how much it insists visually, never what
|
|
28606
|
+
* an outside click does or whether the page behind stays reachable.
|
|
28531
28607
|
* @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
|
|
28532
28608
|
* dialog so the page/container behind it can't scroll while it's open.
|
|
28533
28609
|
* A `layer="local"` dialog always locks its own positioned ancestor's
|
|
@@ -28775,6 +28851,11 @@ const useDialogProps = props => {
|
|
|
28775
28851
|
// there's no native inert-ing, so the real backdrop below is what
|
|
28776
28852
|
// actually makes "capture"/"none" behave the same way here too.
|
|
28777
28853
|
pointerInteractionOutsideEffect = "close",
|
|
28854
|
+
// How loudly the backdrop says it is there — independent of what it
|
|
28855
|
+
// *does* (that's pointerInteractionOutsideEffect above). A dialog is
|
|
28856
|
+
// always modal, so "none" here never makes the page behind reachable:
|
|
28857
|
+
// it only stops the dim from being drawn.
|
|
28858
|
+
backdropAppearance = "auto",
|
|
28778
28859
|
scrollCapture: scrollCaptureProp,
|
|
28779
28860
|
// "auto" (default) → the dialog follows its content. "frozen" → measured
|
|
28780
28861
|
// once, held at that size while open. See this prop's own JSDoc above.
|
|
@@ -29293,7 +29374,8 @@ const useDialogProps = props => {
|
|
|
29293
29374
|
"navi-hidden": openController.opened ? undefined : "",
|
|
29294
29375
|
"styleCSSVars": DIALOG_STYLE_CSS_VARS,
|
|
29295
29376
|
"animationDuration": rest.animationDuration,
|
|
29296
|
-
"data-pointer-interaction-outside": pointerInteractionOutsideEffect
|
|
29377
|
+
"data-pointer-interaction-outside": pointerInteractionOutsideEffect,
|
|
29378
|
+
"data-backdrop-appearance": backdropAppearance
|
|
29297
29379
|
});
|
|
29298
29380
|
Object.assign(contentProps, {
|
|
29299
29381
|
tabIndex,
|
|
@@ -29322,6 +29404,11 @@ const useDialogProps = props => {
|
|
|
29322
29404
|
// real backdrop element already gets the same attribute via
|
|
29323
29405
|
// backdropProps above, which is what its own CSS actually keys off).
|
|
29324
29406
|
"data-pointer-interaction-outside": pointerInteractionOutsideEffect,
|
|
29407
|
+
// Only load-bearing for the via-attribute renderer's own native
|
|
29408
|
+
// ::backdrop, same "a pseudo-element can't carry attributes" reasoning
|
|
29409
|
+
// as the prop just above (and harmless for the custom renderer, whose
|
|
29410
|
+
// real backdrop element gets it via backdropProps).
|
|
29411
|
+
"data-backdrop-appearance": backdropAppearance,
|
|
29325
29412
|
"styleCSSVars": DIALOG_STYLE_CSS_VARS,
|
|
29326
29413
|
...rest,
|
|
29327
29414
|
...autoFocusProps,
|
|
@@ -29724,6 +29811,20 @@ const css$U = /* css */`
|
|
|
29724
29811
|
backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
|
|
29725
29812
|
}
|
|
29726
29813
|
|
|
29814
|
+
/* backdropAppearance overrides whatever the effect above picked — same
|
|
29815
|
+
specificity (class + one attribute), so these have to stay *after*
|
|
29816
|
+
them to win. Only the paint changes: the element is still rendered
|
|
29817
|
+
and still pointer-events: auto, so an outside click keeps doing
|
|
29818
|
+
exactly what pointerInteractionOutsideEffect says. */
|
|
29819
|
+
&[data-backdrop-appearance="discrete"] {
|
|
29820
|
+
background: var(--navi-backdrop-discrete-background);
|
|
29821
|
+
backdrop-filter: none;
|
|
29822
|
+
}
|
|
29823
|
+
&[data-backdrop-appearance="none"] {
|
|
29824
|
+
background: transparent;
|
|
29825
|
+
backdrop-filter: none;
|
|
29826
|
+
}
|
|
29827
|
+
|
|
29727
29828
|
/* navi-animation mirrors the content popover's own resolved value (set
|
|
29728
29829
|
imperatively in openEffect) — the backdrop only ever fades, regardless
|
|
29729
29830
|
of which kind it is (translate/scale wouldn't mean anything on it).
|
|
@@ -29787,6 +29888,15 @@ const css$U = /* css */`
|
|
|
29787
29888
|
* absorbs the click (dims the backdrop) without closing. Note this
|
|
29788
29889
|
* default differs from `Dialog`'s own (`"close"`) — a popover is
|
|
29789
29890
|
* typically a lightweight, non-modal affordance.
|
|
29891
|
+
* @param {"auto"|"discrete"|"none"} [props.backdropAppearance="auto"] - How
|
|
29892
|
+
* visible the backdrop is, independently of what it does. `"auto"`: the
|
|
29893
|
+
* paint `pointerInteractionOutsideEffect` implies (dimmed for
|
|
29894
|
+
* `"close"`/`"cancel"`, blurred glass for `"capture"`). `"discrete"`: a
|
|
29895
|
+
* barely-there dim. `"none"`: fully transparent. The backdrop is still
|
|
29896
|
+
* rendered and still catches outside clicks in every case — this only
|
|
29897
|
+
* changes how much the popover insists on being the thing you deal with.
|
|
29898
|
+
* Ignored when `pointerInteractionOutsideEffect="none"` (there is no
|
|
29899
|
+
* backdrop at all then, and outside clicks pass through).
|
|
29790
29900
|
* @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
|
|
29791
29901
|
* popover so the page/container behind it can't scroll while it's open.
|
|
29792
29902
|
* @param {boolean} [props.focusCapture] - Traps Tab navigation inside the
|
|
@@ -30031,6 +30141,11 @@ const usePopoverProps = props => {
|
|
|
30031
30141
|
// "capture"→ absorb the press, stay open
|
|
30032
30142
|
// "none" → no backdrop
|
|
30033
30143
|
pointerInteractionOutsideEffect = "none",
|
|
30144
|
+
// How loudly the backdrop says it is there — independent of what it
|
|
30145
|
+
// *does* (that's pointerInteractionOutsideEffect above). "auto" keeps
|
|
30146
|
+
// the paint the effect implies; "discrete"/"none" tone it down or
|
|
30147
|
+
// remove it entirely without giving up the outside click.
|
|
30148
|
+
backdropAppearance = "auto",
|
|
30034
30149
|
scrollCapture,
|
|
30035
30150
|
focusCapture,
|
|
30036
30151
|
// "auto" (default) → the popover follows its content. "frozen" → measured
|
|
@@ -30704,6 +30819,7 @@ const usePopoverProps = props => {
|
|
|
30704
30819
|
"styleCSSVars": POPUP_STYLE_CSS_VARS,
|
|
30705
30820
|
"animationDuration": rest.animationDuration,
|
|
30706
30821
|
"data-pointer-interaction-outside": pointerInteractionOutsideEffect,
|
|
30822
|
+
"data-backdrop-appearance": backdropAppearance,
|
|
30707
30823
|
"onMouseDown": mouseDownEvent => {
|
|
30708
30824
|
if (mouseDownEvent.button !== 0) {
|
|
30709
30825
|
return;
|
|
@@ -47374,6 +47490,86 @@ const Editable = props => {
|
|
|
47374
47490
|
});
|
|
47375
47491
|
};
|
|
47376
47492
|
|
|
47493
|
+
/**
|
|
47494
|
+
* What a control HOLDS, as opposed to what it is showing.
|
|
47495
|
+
*
|
|
47496
|
+
* A `value` is held: the control was given it, so handing it back says nothing
|
|
47497
|
+
* new. A `defaultValue` is only a suggestion — an age that is usually 18, a
|
|
47498
|
+
* duration that is usually 1h30 — so the control holds nothing, and confirming
|
|
47499
|
+
* the suggestion IS an answer ("yes, 18"). A control bound to a signal falls on
|
|
47500
|
+
* whichever side the signal put it: a signal with something in it is an answer
|
|
47501
|
+
* (restored from the url, set by whoever owns it), an empty one leaves the
|
|
47502
|
+
* control on its suggestion.
|
|
47503
|
+
*
|
|
47504
|
+
* The same distinction Form makes across its fields (see readHeldUIState in
|
|
47505
|
+
* form.jsx), asked of a single control — which is what lets a Picker tell "the
|
|
47506
|
+
* user re-confirmed what was already chosen" (nothing new) from "the user
|
|
47507
|
+
* accepted the proposal" (an answer).
|
|
47508
|
+
*/
|
|
47509
|
+
|
|
47510
|
+
const isUIStateHeld = (controller) => {
|
|
47511
|
+
if (!controller) {
|
|
47512
|
+
return false;
|
|
47513
|
+
}
|
|
47514
|
+
// Given a value outright: held, whatever it is showing.
|
|
47515
|
+
if (controller.hasStateProp || controller.hasValueProp) {
|
|
47516
|
+
return true;
|
|
47517
|
+
}
|
|
47518
|
+
// A facade (a picker) shows what the control inside its popup holds, so that
|
|
47519
|
+
// is the one to ask — the facade itself was given nothing.
|
|
47520
|
+
const facadeChild = controller.facadeChild;
|
|
47521
|
+
if (facadeChild) {
|
|
47522
|
+
return isUIStateHeld(facadeChild);
|
|
47523
|
+
}
|
|
47524
|
+
const boundSignal = controller.props?.signal;
|
|
47525
|
+
if (boundSignal) {
|
|
47526
|
+
return boundSignal.value !== undefined;
|
|
47527
|
+
}
|
|
47528
|
+
// Uncontrolled with a suggestion: what it shows is that suggestion until it
|
|
47529
|
+
// differs from it.
|
|
47530
|
+
if (controller.defaultValue !== undefined) {
|
|
47531
|
+
return !compareTwoJsValues(controller.uiState, controller.defaultValue);
|
|
47532
|
+
}
|
|
47533
|
+
// A group holding nothing of its own is worth what its children are: two
|
|
47534
|
+
// wheels each on their own suggestion make a group still waiting for an
|
|
47535
|
+
// answer, one of them moved makes a group holding one.
|
|
47536
|
+
const childControllers = controller.getChildControllers?.() || [];
|
|
47537
|
+
if (childControllers.length > 0) {
|
|
47538
|
+
return childControllers.some((child) => isUIStateHeld(child));
|
|
47539
|
+
}
|
|
47540
|
+
return controller.uiState !== undefined;
|
|
47541
|
+
};
|
|
47542
|
+
|
|
47543
|
+
/**
|
|
47544
|
+
* Tell a control — and everything inside it — that what it is showing is now
|
|
47545
|
+
* the answer, without its state having to move.
|
|
47546
|
+
*
|
|
47547
|
+
* The state is already right; what has not happened is anyone saying so. A
|
|
47548
|
+
* control reports an answer through `onUIAction` (that is where a bound signal
|
|
47549
|
+
* is written, where `uiAction` fires), and a suggestion nobody touched never
|
|
47550
|
+
* got there. Confirming a picker is exactly that moment.
|
|
47551
|
+
*
|
|
47552
|
+
* Down the whole subtree because that is where the answer actually lives: a
|
|
47553
|
+
* picker holding a group of two wheels has one signal per wheel, and it is each
|
|
47554
|
+
* wheel that has to record what it is showing. Commands are skipped — a
|
|
47555
|
+
* `command` on a control is its reaction to being used, and this is a
|
|
47556
|
+
* confirmation happening elsewhere, whose own command (the picker's) is already
|
|
47557
|
+
* running.
|
|
47558
|
+
*/
|
|
47559
|
+
const commitUIStateAsAnswer = (controller, e) => {
|
|
47560
|
+
if (!controller) {
|
|
47561
|
+
return;
|
|
47562
|
+
}
|
|
47563
|
+
const answering = controller.facadeChild || controller;
|
|
47564
|
+
commitSubtree(answering, e);
|
|
47565
|
+
};
|
|
47566
|
+
const commitSubtree = (controller, e) => {
|
|
47567
|
+
controller.onUIAction?.(e, { skipCommand: true });
|
|
47568
|
+
for (const child of controller.getChildControllers?.() || []) {
|
|
47569
|
+
commitSubtree(child, e);
|
|
47570
|
+
}
|
|
47571
|
+
};
|
|
47572
|
+
|
|
47377
47573
|
/**
|
|
47378
47574
|
*
|
|
47379
47575
|
* Here we want the same behaviour as web standards:
|
|
@@ -47424,6 +47620,7 @@ const useFormGroup = props => {
|
|
|
47424
47620
|
};
|
|
47425
47621
|
delete propsForGroup.standalone;
|
|
47426
47622
|
delete propsForGroup.canSendWhileUnchanged;
|
|
47623
|
+
delete propsForGroup.pristineKey;
|
|
47427
47624
|
// Not the generic control `command`, which a control triggers on its own ui
|
|
47428
47625
|
// actions — here it is what follows a SUCCESSFUL submission. So it is kept
|
|
47429
47626
|
// out of the control machinery and left in the DOM for the send to read
|
|
@@ -47456,7 +47653,7 @@ const useFormGroup = props => {
|
|
|
47456
47653
|
// microtask away, so typing and pressing Enter right after must not be read
|
|
47457
47654
|
// against the state of the previous frame.
|
|
47458
47655
|
uiStateController.shouldRequestAction = value => Boolean(props.canSendWhileUnchanged) || !compareTwoJsValues(withoutEmptyFields(value), uiStateController.sentUIState);
|
|
47459
|
-
|
|
47656
|
+
useHeldUIStateAsSent(uiStateController, props.pristineKey);
|
|
47460
47657
|
useUnregisteredControlWarning(props.ref);
|
|
47461
47658
|
const {
|
|
47462
47659
|
basePseudoState,
|
|
@@ -47554,13 +47751,10 @@ const FormNested = props => {
|
|
|
47554
47751
|
});
|
|
47555
47752
|
};
|
|
47556
47753
|
|
|
47557
|
-
// What the form HOLDS, as opposed to what it is showing
|
|
47558
|
-
//
|
|
47559
|
-
//
|
|
47560
|
-
//
|
|
47561
|
-
// back IS an answer ("yes, 18"). A field bound to a signal falls on whichever
|
|
47562
|
-
// side the signal put it: one carrying a default seeds `defaultValue`, one
|
|
47563
|
-
// without controls the field outright.
|
|
47754
|
+
// What the form HOLDS, as opposed to what it is showing — field by field, the
|
|
47755
|
+
// question isUIStateHeld answers: a field it was given an answer for is held,
|
|
47756
|
+
// a field merely showing a suggestion is not, and confirming that suggestion IS
|
|
47757
|
+
// an answer ("yes, 18").
|
|
47564
47758
|
const readHeldUIState = uiStateController => {
|
|
47565
47759
|
const uiState = uiStateController.uiState;
|
|
47566
47760
|
// A form given a value holds all of it, whatever its fields say.
|
|
@@ -47571,7 +47765,7 @@ const readHeldUIState = uiStateController => {
|
|
|
47571
47765
|
...uiState
|
|
47572
47766
|
};
|
|
47573
47767
|
for (const child of uiStateController.getChildControllers?.() || []) {
|
|
47574
|
-
if (child.name && !child
|
|
47768
|
+
if (child.name && !isUIStateHeld(child)) {
|
|
47575
47769
|
delete held[child.name];
|
|
47576
47770
|
}
|
|
47577
47771
|
}
|
|
@@ -47601,10 +47795,22 @@ const withoutEmptyFields = uiState => {
|
|
|
47601
47795
|
// register themselves in their own effects, which run first — this is the
|
|
47602
47796
|
// earliest moment the form knows what it holds. Everything after this baseline
|
|
47603
47797
|
// is a real send moving it forward (see useFormGroup's own onnavi_action_end).
|
|
47604
|
-
const
|
|
47798
|
+
const useHeldUIStateAsSent = (uiStateController, pristineKey) => {
|
|
47799
|
+
// The render that brought a new pristineKey read `changed` against the
|
|
47800
|
+
// previous baseline, and nothing else is going to move: the button would stay
|
|
47801
|
+
// lit on a form that holds exactly what it was just given. So ask for the one
|
|
47802
|
+
// render that reads the new baseline — the first one has nobody to tell,
|
|
47803
|
+
// every field it is waiting for re-renders the form as it registers.
|
|
47804
|
+
const [, rereadBaseline] = useState(0);
|
|
47805
|
+
const isFirstRef = useRef(true);
|
|
47605
47806
|
useLayoutEffect(() => {
|
|
47606
47807
|
uiStateController.sentUIState = readHeldUIState(uiStateController);
|
|
47607
|
-
|
|
47808
|
+
if (isFirstRef.current) {
|
|
47809
|
+
isFirstRef.current = false;
|
|
47810
|
+
return;
|
|
47811
|
+
}
|
|
47812
|
+
rereadBaseline(count => count + 1);
|
|
47813
|
+
}, [uiStateController, pristineKey]);
|
|
47608
47814
|
};
|
|
47609
47815
|
const useUnregisteredControlWarning = ref => {
|
|
47610
47816
|
// No dependency array: fields appear and disappear as the form re-renders,
|
|
@@ -47914,6 +48120,29 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
47914
48120
|
* container of ours could ever hold two of them side by side and translate the
|
|
47915
48121
|
* pair. One popup holding slides of its own contents has no such problem — and
|
|
47916
48122
|
* it is the same component in the document, in a dialog or in a popover.
|
|
48123
|
+
*
|
|
48124
|
+
* Padding belongs on the SLIDE, never on this box nor on anything above it.
|
|
48125
|
+
* Overflow clips at the PADDING edge, so a padding given to the container is a
|
|
48126
|
+
* band the clipping does not cover: a slide travelling through it is seen there
|
|
48127
|
+
* before it has reached the frame. And a padding above the slides does not
|
|
48128
|
+
* travel — the two contents crossing each other pass with nothing between them,
|
|
48129
|
+
* each flush against the other, instead of arriving already inset. Put on the
|
|
48130
|
+
* slide, the inset moves with what it insets, and a travel shows two paddings'
|
|
48131
|
+
* worth of gutter between the two.
|
|
48132
|
+
*
|
|
48133
|
+
* What SCROLLS is the slide too, and for the same reason read the other way
|
|
48134
|
+
* round: the box is as big as its largest slide, so a scroller placed above the
|
|
48135
|
+
* slides is always scrolling the tallest of them — stand on a short one and it
|
|
48136
|
+
* carries the scrollbar of a neighbour, scrolling through emptiness. The cap on
|
|
48137
|
+
* the height comes from above (a max-height on the popup, a column it has to
|
|
48138
|
+
* fit in) and must reach the slides as a CONSTRAINT rather than as a scroller:
|
|
48139
|
+
* this box shrinks into it (flex: 0 1 auto below), the grid hands that height
|
|
48140
|
+
* to every slide, and a slide with an overflow of its own scrolls only when ITS
|
|
48141
|
+
* content is taller than that. The tall slide scrolls; the short ones are tall
|
|
48142
|
+
* boxes with a short content in them, which is exactly what one wants — they
|
|
48143
|
+
* take the height the context imposes and ignore the height of their neighbour.
|
|
48144
|
+
* So: nothing scrollable between the cap and the slides (a shared [data-body]
|
|
48145
|
+
* around them IS a scroller, see box.jsx), and `overflow="auto"` on each Slide.
|
|
47917
48146
|
*/
|
|
47918
48147
|
const css$A = /* css */`
|
|
47919
48148
|
/* Where the picture stands relative to the slide that is current, in boxes
|
|
@@ -51238,6 +51467,11 @@ const css$z = /* css */`
|
|
|
51238
51467
|
* is unavoidably *more* intrusive once it switches to dialog mode than
|
|
51239
51468
|
* the exact same usage would be as a popover — worth keeping in mind for
|
|
51240
51469
|
* anything that relies on `Popup` and can end up on a small screen.
|
|
51470
|
+
* @param {"auto"|"discrete"|"none"} [props.backdropAppearance] - Forwarded
|
|
51471
|
+
* as-is to whichever component renders (both understand it identically):
|
|
51472
|
+
* how visible the backdrop is, independently of what an outside click
|
|
51473
|
+
* does. Unlike `pointerInteractionOutsideEffect` above, this one needs no
|
|
51474
|
+
* default here — `"auto"` already means the same thing on both sides.
|
|
51241
51475
|
* @param {boolean|"auto"|"fading"|"scaling"|"sliding"|"expanding"|`slide-from-${string}`|`expand-${string}`} [props.animation]
|
|
51242
51476
|
* - Forwarded as-is.
|
|
51243
51477
|
* @param {string} [props.animationDuration] - Forwarded as-is.
|
|
@@ -51344,86 +51578,6 @@ const Popup = props => {
|
|
|
51344
51578
|
});
|
|
51345
51579
|
};
|
|
51346
51580
|
|
|
51347
|
-
/**
|
|
51348
|
-
* What a control HOLDS, as opposed to what it is showing.
|
|
51349
|
-
*
|
|
51350
|
-
* A `value` is held: the control was given it, so handing it back says nothing
|
|
51351
|
-
* new. A `defaultValue` is only a suggestion — an age that is usually 18, a
|
|
51352
|
-
* duration that is usually 1h30 — so the control holds nothing, and confirming
|
|
51353
|
-
* the suggestion IS an answer ("yes, 18"). A control bound to a signal falls on
|
|
51354
|
-
* whichever side the signal put it: a signal with something in it is an answer
|
|
51355
|
-
* (restored from the url, set by whoever owns it), an empty one leaves the
|
|
51356
|
-
* control on its suggestion.
|
|
51357
|
-
*
|
|
51358
|
-
* The same distinction Form makes across its fields (see readHeldUIState in
|
|
51359
|
-
* form.jsx), asked of a single control — which is what lets a Picker tell "the
|
|
51360
|
-
* user re-confirmed what was already chosen" (nothing new) from "the user
|
|
51361
|
-
* accepted the proposal" (an answer).
|
|
51362
|
-
*/
|
|
51363
|
-
|
|
51364
|
-
const isUIStateHeld = (controller) => {
|
|
51365
|
-
if (!controller) {
|
|
51366
|
-
return false;
|
|
51367
|
-
}
|
|
51368
|
-
// Given a value outright: held, whatever it is showing.
|
|
51369
|
-
if (controller.hasStateProp || controller.hasValueProp) {
|
|
51370
|
-
return true;
|
|
51371
|
-
}
|
|
51372
|
-
// A facade (a picker) shows what the control inside its popup holds, so that
|
|
51373
|
-
// is the one to ask — the facade itself was given nothing.
|
|
51374
|
-
const facadeChild = controller.facadeChild;
|
|
51375
|
-
if (facadeChild) {
|
|
51376
|
-
return isUIStateHeld(facadeChild);
|
|
51377
|
-
}
|
|
51378
|
-
const boundSignal = controller.props?.signal;
|
|
51379
|
-
if (boundSignal) {
|
|
51380
|
-
return boundSignal.value !== undefined;
|
|
51381
|
-
}
|
|
51382
|
-
// Uncontrolled with a suggestion: what it shows is that suggestion until it
|
|
51383
|
-
// differs from it.
|
|
51384
|
-
if (controller.defaultValue !== undefined) {
|
|
51385
|
-
return !compareTwoJsValues(controller.uiState, controller.defaultValue);
|
|
51386
|
-
}
|
|
51387
|
-
// A group holding nothing of its own is worth what its children are: two
|
|
51388
|
-
// wheels each on their own suggestion make a group still waiting for an
|
|
51389
|
-
// answer, one of them moved makes a group holding one.
|
|
51390
|
-
const childControllers = controller.getChildControllers?.() || [];
|
|
51391
|
-
if (childControllers.length > 0) {
|
|
51392
|
-
return childControllers.some((child) => isUIStateHeld(child));
|
|
51393
|
-
}
|
|
51394
|
-
return controller.uiState !== undefined;
|
|
51395
|
-
};
|
|
51396
|
-
|
|
51397
|
-
/**
|
|
51398
|
-
* Tell a control — and everything inside it — that what it is showing is now
|
|
51399
|
-
* the answer, without its state having to move.
|
|
51400
|
-
*
|
|
51401
|
-
* The state is already right; what has not happened is anyone saying so. A
|
|
51402
|
-
* control reports an answer through `onUIAction` (that is where a bound signal
|
|
51403
|
-
* is written, where `uiAction` fires), and a suggestion nobody touched never
|
|
51404
|
-
* got there. Confirming a picker is exactly that moment.
|
|
51405
|
-
*
|
|
51406
|
-
* Down the whole subtree because that is where the answer actually lives: a
|
|
51407
|
-
* picker holding a group of two wheels has one signal per wheel, and it is each
|
|
51408
|
-
* wheel that has to record what it is showing. Commands are skipped — a
|
|
51409
|
-
* `command` on a control is its reaction to being used, and this is a
|
|
51410
|
-
* confirmation happening elsewhere, whose own command (the picker's) is already
|
|
51411
|
-
* running.
|
|
51412
|
-
*/
|
|
51413
|
-
const commitUIStateAsAnswer = (controller, e) => {
|
|
51414
|
-
if (!controller) {
|
|
51415
|
-
return;
|
|
51416
|
-
}
|
|
51417
|
-
const answering = controller.facadeChild || controller;
|
|
51418
|
-
commitSubtree(answering, e);
|
|
51419
|
-
};
|
|
51420
|
-
const commitSubtree = (controller, e) => {
|
|
51421
|
-
controller.onUIAction?.(e, { skipCommand: true });
|
|
51422
|
-
for (const child of controller.getChildControllers?.() || []) {
|
|
51423
|
-
commitSubtree(child, e);
|
|
51424
|
-
}
|
|
51425
|
-
};
|
|
51426
|
-
|
|
51427
51581
|
installImportMetaCssBuild(import.meta);const css$y = /* css */`
|
|
51428
51582
|
.navi_picker {
|
|
51429
51583
|
/* Sizing ceilings (maxmax), background, box-shadow, outline, padding,
|
|
@@ -52039,6 +52193,9 @@ const PickerContentInsidePopup = props => {
|
|
|
52039
52193
|
// action if the value changed) — Escape still cancels. Pass "cancel" to make
|
|
52040
52194
|
// clicking outside revert instead, or "capture" to keep it open.
|
|
52041
52195
|
pointerInteractionOutsideEffect = "close",
|
|
52196
|
+
// Named/forwarded rather than left in ...rest: rest goes to the picker
|
|
52197
|
+
// element itself, not the popup, and this belongs to the popup.
|
|
52198
|
+
backdropAppearance,
|
|
52042
52199
|
dialogExpand,
|
|
52043
52200
|
dialogExpandX,
|
|
52044
52201
|
dialogExpandY,
|
|
@@ -52089,6 +52246,7 @@ const PickerContentInsidePopup = props => {
|
|
|
52089
52246
|
marginWithContainer: marginWithContainer === undefined && isPopover ? popoverSpacing : marginWithContainer,
|
|
52090
52247
|
scrollCapture: scrollCapture,
|
|
52091
52248
|
pointerInteractionOutsideEffect: pointerLock ? "capture" : pointerInteractionOutsideEffect,
|
|
52249
|
+
backdropAppearance: backdropAppearance,
|
|
52092
52250
|
focusCapture: isPopover ? focusCapture : undefined,
|
|
52093
52251
|
expand: isPopover ? undefined : dialogExpand,
|
|
52094
52252
|
expandX: isPopover ? undefined : dialogExpandX,
|
|
@@ -53711,6 +53869,28 @@ const css$v = /* css */`
|
|
|
53711
53869
|
--list-border-width-default: 1px;
|
|
53712
53870
|
--list-border-color: light-dark(#ccc, #555);
|
|
53713
53871
|
--list-background-color: light-dark(#fff, #1e1e1e);
|
|
53872
|
+
|
|
53873
|
+
/* A sticky part paints over the rows only while it IS stuck — which is
|
|
53874
|
+
what --navi-z-index-sticky says it is for ("kept stuck while something
|
|
53875
|
+
scrolls under it"). At rest it is a block in the flow with nothing
|
|
53876
|
+
passing under it, and a 10 there is what slices whatever a neighbouring
|
|
53877
|
+
row lets out of its box: a focus ring, a badge, a stamp. See
|
|
53878
|
+
useStuckStickyParts for the navi-stuck attribute these read.
|
|
53879
|
+
|
|
53880
|
+
With "auto" at rest, a card whose badge overflows into the label
|
|
53881
|
+
below it gets past it by saying z-index: 1 on that badge — a literal,
|
|
53882
|
+
in the card, against its own neighbour, which is what docs/z_index.md
|
|
53883
|
+
asks for. These variables are the escape hatch for what that cannot
|
|
53884
|
+
reach, not the usual answer. Mind that a negative value here is
|
|
53885
|
+
compared against the page: it needs a stacking context between the
|
|
53886
|
+
label and the nearest opaque background, or the label goes behind that
|
|
53887
|
+
background and disappears. */
|
|
53888
|
+
--list-header-z-index: auto;
|
|
53889
|
+
--list-header-z-index-stuck: var(--navi-z-index-sticky);
|
|
53890
|
+
--list-footer-z-index: auto;
|
|
53891
|
+
--list-footer-z-index-stuck: var(--navi-z-index-sticky);
|
|
53892
|
+
--list-group-label-z-index: auto;
|
|
53893
|
+
--list-group-label-z-index-stuck: var(--navi-z-index-sticky);
|
|
53714
53894
|
}
|
|
53715
53895
|
.navi_list_item {
|
|
53716
53896
|
--list-item-padding-x-default: 0px;
|
|
@@ -54092,8 +54272,12 @@ const css$v = /* css */`
|
|
|
54092
54272
|
position: sticky;
|
|
54093
54273
|
top: 0;
|
|
54094
54274
|
left: 0;
|
|
54095
|
-
z-index: var(--
|
|
54275
|
+
z-index: var(--list-header-z-index);
|
|
54096
54276
|
order: -2;
|
|
54277
|
+
|
|
54278
|
+
&[navi-stuck] {
|
|
54279
|
+
z-index: var(--list-header-z-index-stuck);
|
|
54280
|
+
}
|
|
54097
54281
|
}
|
|
54098
54282
|
.navi_list_fallback,
|
|
54099
54283
|
.navi_list_search_fallback {
|
|
@@ -54199,8 +54383,12 @@ const css$v = /* css */`
|
|
|
54199
54383
|
position: sticky;
|
|
54200
54384
|
right: 0;
|
|
54201
54385
|
bottom: 0;
|
|
54202
|
-
z-index: var(--
|
|
54386
|
+
z-index: var(--list-footer-z-index);
|
|
54203
54387
|
order: 2;
|
|
54388
|
+
|
|
54389
|
+
&[navi-stuck] {
|
|
54390
|
+
z-index: var(--list-footer-z-index-stuck);
|
|
54391
|
+
}
|
|
54204
54392
|
}
|
|
54205
54393
|
|
|
54206
54394
|
::highlight(navi-search-match) {
|
|
@@ -54215,11 +54403,15 @@ const css$v = /* css */`
|
|
|
54215
54403
|
.navi_list_item_group_label {
|
|
54216
54404
|
position: sticky;
|
|
54217
54405
|
top: var(--list-group-label-top, var(--x-list-group-label-top, 0px));
|
|
54218
|
-
z-index: var(--
|
|
54406
|
+
z-index: var(--list-group-label-z-index);
|
|
54219
54407
|
display: block;
|
|
54220
54408
|
background-color: var(--list-group-label-background-color);
|
|
54221
54409
|
user-select: none;
|
|
54222
54410
|
|
|
54411
|
+
&[navi-stuck] {
|
|
54412
|
+
z-index: var(--list-group-label-z-index-stuck);
|
|
54413
|
+
}
|
|
54414
|
+
|
|
54223
54415
|
&[navi-default] {
|
|
54224
54416
|
padding: 4px 12px 2px;
|
|
54225
54417
|
color: light-dark(#888, #aaa);
|
|
@@ -54837,6 +55029,7 @@ const useListScrollSync = ({
|
|
|
54837
55029
|
};
|
|
54838
55030
|
useLayoutEffect(resolveScroller);
|
|
54839
55031
|
useStickyScrollportWarning();
|
|
55032
|
+
useStuckStickyParts(ref, getScroller, scrollerElResolved, horizontal);
|
|
54840
55033
|
|
|
54841
55034
|
// The row the scroll holds onto across a change of geometry, and where it
|
|
54842
55035
|
// sat when that change was decided. Captured at the two moments the list
|
|
@@ -55614,6 +55807,14 @@ const getScrollerViewportRect = scrollerEl => {
|
|
|
55614
55807
|
}
|
|
55615
55808
|
return scrollerEl.getBoundingClientRect();
|
|
55616
55809
|
};
|
|
55810
|
+
// What a sticky part of the list sticks to is the nearest scroll container in
|
|
55811
|
+
// the DOM; `scroller` has no say in it. A list told the page scrolls it can
|
|
55812
|
+
// therefore have its group labels and its header stuck to a wrapper that never
|
|
55813
|
+
// scrolls — and pushed down by that wrapper's scroll-padding on top of it. The
|
|
55814
|
+
// usual culprit is an app wrapper carrying `overflow-x: auto` to keep the
|
|
55815
|
+
// document from overflowing horizontally on mobile; `overflow-x: clip` keeps
|
|
55816
|
+
// that guarantee without making a scroll container.
|
|
55817
|
+
const STICKY_LIST_PART_SELECTOR = `.navi_list_item_header, .navi_list_item_footer, .navi_list_item_group_label`;
|
|
55617
55818
|
const useStickyScrollportWarning = (ref, scroller) => {
|
|
55618
55819
|
useRef(false);
|
|
55619
55820
|
useLayoutEffect(() => {
|
|
@@ -55623,6 +55824,142 @@ const useStickyScrollportWarning = (ref, scroller) => {
|
|
|
55623
55824
|
});
|
|
55624
55825
|
};
|
|
55625
55826
|
|
|
55827
|
+
/**
|
|
55828
|
+
* "Am I stuck?" — the question a `position: sticky` element cannot ask about
|
|
55829
|
+
* itself. There is no selector for it, and `scroll-state(stuck: top)` does not
|
|
55830
|
+
* answer it either: that query styles a container's DESCENDANTS, so a part
|
|
55831
|
+
* cannot read its own stuck state, which is exactly the one a background, a
|
|
55832
|
+
* shadow or a stacking order has to depend on.
|
|
55833
|
+
*
|
|
55834
|
+
* So the list says it, on the three parts it makes sticky: `navi-stuck` while a
|
|
55835
|
+
* part sits at the edge it sticks to, gone while it rides along in the flow.
|
|
55836
|
+
* The list is the right place for it because it is the only one that knows
|
|
55837
|
+
* WHICH box its parts stick to — an app writing this outside would listen to
|
|
55838
|
+
* the window and be right only for `scroller="document"` (see getScrollerEl,
|
|
55839
|
+
* and useStickyScrollportWarning for the case where even the list is wrong
|
|
55840
|
+
* about it: a scroll container between the two, which dev mode reports).
|
|
55841
|
+
*
|
|
55842
|
+
* What reads it is navi's own z-index rule first (see --list-*-z-index above:
|
|
55843
|
+
* the sticky band is for a part with something scrolling under it, not for a
|
|
55844
|
+
* block at rest in the flow), and an app second, for anything it wants to say
|
|
55845
|
+
* about a part being stuck.
|
|
55846
|
+
*/
|
|
55847
|
+
// Fractional layout is the rule, not the exception — zoom, screen density, a
|
|
55848
|
+
// scroller at a half-pixel offset. A part at its sticky offset can render a
|
|
55849
|
+
// fraction short of it, and without this slack it reads as being at rest: a
|
|
55850
|
+
// bug that shows up on one machine and not the next.
|
|
55851
|
+
const STUCK_SLACK = 1;
|
|
55852
|
+
// Which edge a part sticks to. The header and the footer stick along whichever
|
|
55853
|
+
// axis the list scrolls — their rules declare both insets (top/left, and
|
|
55854
|
+
// bottom/right) so the same markup works either way; a group label always caps
|
|
55855
|
+
// its group from the top.
|
|
55856
|
+
const getStickyEdge = (partEl, horizontal) => {
|
|
55857
|
+
if (partEl.classList.contains("navi_list_item_footer")) {
|
|
55858
|
+
return horizontal ? "right" : "bottom";
|
|
55859
|
+
}
|
|
55860
|
+
if (partEl.classList.contains("navi_list_item_header")) {
|
|
55861
|
+
return horizontal ? "left" : "top";
|
|
55862
|
+
}
|
|
55863
|
+
return "top";
|
|
55864
|
+
};
|
|
55865
|
+
// A sticky inset is measured from the scrollport — the padding box of the
|
|
55866
|
+
// scroller, or the viewport when the page scrolls. getScrollerViewportRect
|
|
55867
|
+
// gives the border box; the borders come off here, since a scroller with one
|
|
55868
|
+
// would otherwise read as a pixel of scrolling already done.
|
|
55869
|
+
const getScrollportRect = scrollerEl => {
|
|
55870
|
+
const rect = getScrollerViewportRect(scrollerEl);
|
|
55871
|
+
if (scrollerEl === document.scrollingElement) {
|
|
55872
|
+
return rect;
|
|
55873
|
+
}
|
|
55874
|
+
const top = rect.top + scrollerEl.clientTop;
|
|
55875
|
+
const left = rect.left + scrollerEl.clientLeft;
|
|
55876
|
+
return {
|
|
55877
|
+
top,
|
|
55878
|
+
left,
|
|
55879
|
+
bottom: top + scrollerEl.clientHeight,
|
|
55880
|
+
right: left + scrollerEl.clientWidth
|
|
55881
|
+
};
|
|
55882
|
+
};
|
|
55883
|
+
const isPartStuck = (partEl, edge, scrollportRect) => {
|
|
55884
|
+
// The inset is read computed, not from the rule: --list-group-label-top and
|
|
55885
|
+
// the FixedBar space behind it are what put the label where it sticks.
|
|
55886
|
+
const declared = parseFloat(getComputedStyle(partEl)[edge]);
|
|
55887
|
+
const inset = Number.isFinite(declared) ? declared : 0;
|
|
55888
|
+
const rect = partEl.getBoundingClientRect();
|
|
55889
|
+
if (edge === "top") {
|
|
55890
|
+
return rect.top - scrollportRect.top <= inset + STUCK_SLACK;
|
|
55891
|
+
}
|
|
55892
|
+
if (edge === "left") {
|
|
55893
|
+
return rect.left - scrollportRect.left <= inset + STUCK_SLACK;
|
|
55894
|
+
}
|
|
55895
|
+
if (edge === "bottom") {
|
|
55896
|
+
return scrollportRect.bottom - rect.bottom <= inset + STUCK_SLACK;
|
|
55897
|
+
}
|
|
55898
|
+
return scrollportRect.right - rect.right <= inset + STUCK_SLACK;
|
|
55899
|
+
};
|
|
55900
|
+
const useStuckStickyParts = (ref, getScroller, scrollerElResolved, horizontal) => {
|
|
55901
|
+
// Rewritten on every render so the listeners below, registered once per
|
|
55902
|
+
// scroller, always run against the current geometry.
|
|
55903
|
+
const updateRef = useRef(null);
|
|
55904
|
+
updateRef.current = () => {
|
|
55905
|
+
const listContainerEl = ref.current;
|
|
55906
|
+
if (!listContainerEl) {
|
|
55907
|
+
return;
|
|
55908
|
+
}
|
|
55909
|
+
const partEls = listContainerEl.querySelectorAll(STICKY_LIST_PART_SELECTOR);
|
|
55910
|
+
if (partEls.length === 0) {
|
|
55911
|
+
return;
|
|
55912
|
+
}
|
|
55913
|
+
const scrollerEl = getScroller();
|
|
55914
|
+
if (!scrollerEl) {
|
|
55915
|
+
return;
|
|
55916
|
+
}
|
|
55917
|
+
// One rect per RENDERED part: virtualization already bounds how many of
|
|
55918
|
+
// them exist, which is what keeps this affordable on every scroll event.
|
|
55919
|
+
const scrollportRect = getScrollportRect(scrollerEl);
|
|
55920
|
+
for (const partEl of partEls) {
|
|
55921
|
+
const edge = getStickyEdge(partEl, horizontal);
|
|
55922
|
+
partEl.toggleAttribute("navi-stuck", isPartStuck(partEl, edge, scrollportRect));
|
|
55923
|
+
}
|
|
55924
|
+
};
|
|
55925
|
+
|
|
55926
|
+
// Every commit, because a virtualized list changes which parts exist without
|
|
55927
|
+
// anything scrolling: group labels enter and leave the DOM as the window
|
|
55928
|
+
// moves, and one that arrives already at the edge has never been measured.
|
|
55929
|
+
useLayoutEffect(() => {
|
|
55930
|
+
updateRef.current();
|
|
55931
|
+
});
|
|
55932
|
+
useLayoutEffect(() => {
|
|
55933
|
+
const listContainerEl = ref.current;
|
|
55934
|
+
if (!listContainerEl) {
|
|
55935
|
+
return undefined;
|
|
55936
|
+
}
|
|
55937
|
+
const update = () => {
|
|
55938
|
+
// Synchronously, not on a rAF: scroll events are dispatched while the
|
|
55939
|
+
// frame is being put together, so the attribute lands in the same paint
|
|
55940
|
+
// as the scroll that caused it. A frame late is a frame of flicker.
|
|
55941
|
+
updateRef.current();
|
|
55942
|
+
};
|
|
55943
|
+
// A page-level scroller does not emit "scroll" on the element itself
|
|
55944
|
+
// (document.scrollingElement); the document does.
|
|
55945
|
+
const scrollerEl = getScroller();
|
|
55946
|
+
const scrollEventTarget = !scrollerEl || scrollerEl === document.scrollingElement ? document : scrollerEl;
|
|
55947
|
+
scrollEventTarget.addEventListener("scroll", update, {
|
|
55948
|
+
passive: true
|
|
55949
|
+
});
|
|
55950
|
+
window.addEventListener("resize", update);
|
|
55951
|
+
// The list growing under a scroller that has not moved — rows loaded by
|
|
55952
|
+
// scroll, a group unfolding — changes which parts sit at an edge.
|
|
55953
|
+
const observer = new ResizeObserver(update);
|
|
55954
|
+
observer.observe(listContainerEl);
|
|
55955
|
+
return () => {
|
|
55956
|
+
scrollEventTarget.removeEventListener("scroll", update);
|
|
55957
|
+
window.removeEventListener("resize", update);
|
|
55958
|
+
observer.disconnect();
|
|
55959
|
+
};
|
|
55960
|
+
}, [scrollerElResolved]);
|
|
55961
|
+
};
|
|
55962
|
+
|
|
55626
55963
|
// The CSS needs to tell "the page scrolls me" from "some box around me
|
|
55627
55964
|
// scrolls me": only the first one sticks to the viewport, where the fixed bars
|
|
55628
55965
|
// are.
|
|
@@ -58958,110 +59295,6 @@ const PickerFirstResolver = props => {
|
|
|
58958
59295
|
...props
|
|
58959
59296
|
});
|
|
58960
59297
|
};
|
|
58961
|
-
|
|
58962
|
-
/**
|
|
58963
|
-
* Button-like trigger that opens a picker (native or custom popup) when clicked.
|
|
58964
|
-
*
|
|
58965
|
-
* Without `children`, opens the browser-native picker for the given `type`.
|
|
58966
|
-
* With `children`, opens a popover (desktop) or dialog (mobile) containing the children.
|
|
58967
|
-
* Pass `mode="popover"` or `mode="dialog"` to override the automatic choice.
|
|
58968
|
-
*
|
|
58969
|
-
* @type {import("ignore:preact").FunctionComponent<{
|
|
58970
|
-
* type?: "date" | "month" | "week" | "time" | "datetime" | "color" | "hour" | "navi_time" | "navi_number" | "navi_percentage",
|
|
58971
|
-
* value?: any,
|
|
58972
|
-
* defaultValue?: any,
|
|
58973
|
-
* name?: string,
|
|
58974
|
-
* placeholder?: import("ignore:preact").ComponentChildren,
|
|
58975
|
-
* required?: boolean,
|
|
58976
|
-
* min?: Date | string | number,
|
|
58977
|
-
* max?: Date | string | number,
|
|
58978
|
-
* step?: string | number,
|
|
58979
|
-
* disabled?: boolean,
|
|
58980
|
-
* readOnly?: boolean,
|
|
58981
|
-
* error?: boolean | string,
|
|
58982
|
-
* uiAction?: (value: any, event: Event) => void,
|
|
58983
|
-
* action?: (value: any, event: Event) => void,
|
|
58984
|
-
* children?: import("ignore:preact").ComponentChildren,
|
|
58985
|
-
* mode?: "popover" | "dialog",
|
|
58986
|
-
* popoverMode?: "nearby" | "overlay",
|
|
58987
|
-
* positionArea?: string,
|
|
58988
|
-
* popupWidthFitContent?: boolean,
|
|
58989
|
-
* variant?: "icon" | "headless" | "discrete",
|
|
58990
|
-
* rightSlotIcon?: import("ignore:preact").ComponentChildren,
|
|
58991
|
-
* rightSlotIconSize?: number | string,
|
|
58992
|
-
* maxLines?: number,
|
|
58993
|
-
* slotSpacing?: number | string,
|
|
58994
|
-
* popoverMaxHeight?: number | string,
|
|
58995
|
-
* dialogMaxWidth?: number | string,
|
|
58996
|
-
* dialogMaxHeight?: number | string,
|
|
58997
|
-
* popupBackgroundColor?: string,
|
|
58998
|
-
* popupBorderRadius?: number | string,
|
|
58999
|
-
* clearable?: boolean,
|
|
59000
|
-
* popupLayer?: "top" | "local",
|
|
59001
|
-
* dialogExpand?: boolean,
|
|
59002
|
-
* dialogExpandX?: boolean,
|
|
59003
|
-
* dialogExpandY?: boolean,
|
|
59004
|
-
* marginWithContainer?: number | string,
|
|
59005
|
-
* escapeEffect?: "cancel" | "close",
|
|
59006
|
-
* pointerInteractionOutsideEffect?: "close" | "cancel" | "capture",
|
|
59007
|
-
* ref?: import("ignore:preact").RefObject<HTMLElement>,
|
|
59008
|
-
* [key: string]: any,
|
|
59009
|
-
* }>}
|
|
59010
|
-
* @param {boolean|string} [error] Something went wrong around this picker (its
|
|
59011
|
-
* content failed to load, its value could not be resolved…). Shown as a
|
|
59012
|
-
* callout on the trigger, open or closed — the caller has nothing to place.
|
|
59013
|
-
* Dismissing it discards that error; a new `error` value raises another one.
|
|
59014
|
-
* @param {"nearby"|"overlay"} [popoverMode="nearby"] "overlay" lays the popover
|
|
59015
|
-
* over the trigger, "nearby" leaves a small gap below it.
|
|
59016
|
-
* @param {string} [positionArea] Where the popup goes — relative to the trigger
|
|
59017
|
-
* in popover mode, relative to the viewport in dialog mode. Same grammar as
|
|
59018
|
-
* Popover/Dialog's own `positionArea` ("top", "right-end", "inset(top-left)",
|
|
59019
|
-
* …). Defaults to "bottom-start" in popover mode ("inset(top-left)" when
|
|
59020
|
-
* popoverMode is "overlay"), and to Dialog's own "center" in dialog mode. A
|
|
59021
|
-
* popover still flips to the opposite side on its own when there isn't
|
|
59022
|
-
* enough room.
|
|
59023
|
-
* @param {boolean} [popupWidthFitContent] By default the popup is at least as
|
|
59024
|
-
* wide as the trigger. Set this to let the content size it instead, so a
|
|
59025
|
-
* popup narrower than the trigger stays narrow.
|
|
59026
|
-
* @param {import("ignore:preact").ComponentChildren} [rightSlotIcon] What the right
|
|
59027
|
-
* slot draws in place of the chevron. It is the whole slot, not an addition
|
|
59028
|
-
* to it: the picker then no longer says on its own that it opens, so pass
|
|
59029
|
-
* something that does.
|
|
59030
|
-
* @param {number|string} [rightSlotIconSize="inherit"] How big what sits in the
|
|
59031
|
-
* right slot is drawn — the chevron, a `rightSlotIcon`, or the clear button's
|
|
59032
|
-
* cross. "inherit" takes the picker's own font size.
|
|
59033
|
-
* @param {number|string} [slotSpacing] Gap kept between what sits in the right
|
|
59034
|
-
* slot (the chevron, or the clear button) and the picker's own edge — same
|
|
59035
|
-
* prop name Input uses for its own slots. Accepts a spacing token ("s",
|
|
59036
|
-
* "m"…) like any other spacing prop, or a length. Defaults to half the
|
|
59037
|
-
* horizontal padding.
|
|
59038
|
-
* @param {number|string} [popoverMaxHeight] Soft cap on the popover's height
|
|
59039
|
-
* (default 300px). The popover shrinks below it when space is tight.
|
|
59040
|
-
* @param {number|string} [dialogMaxWidth] Ceiling on the dialog's width, the
|
|
59041
|
-
* one `dialogExpand`/`dialogExpandX` grows up to — what makes an expanded
|
|
59042
|
-
* dialog a large sheet rather than a full screen. Capped in turn by the
|
|
59043
|
-
* container minus `marginWithContainer`.
|
|
59044
|
-
* @param {number|string} [dialogMaxHeight] Same, on the height.
|
|
59045
|
-
* @param {"cancel"|"close"} [escapeEffect="cancel"] What Escape does to an open
|
|
59046
|
-
* picker. "cancel" puts back the value the picker had at open, and a dialog
|
|
59047
|
-
* picker also goes back in history — so anything written to the url while it
|
|
59048
|
-
* was open (a route `stateSignal`, a search param) goes back with it. "close"
|
|
59049
|
-
* makes Escape say what clicking outside says: keep what was chosen, close —
|
|
59050
|
-
* a last resort, see docs/popup_open.md ("Escape cancels, the other gestures
|
|
59051
|
-
* keep") for why Escape should go on meaning cancel, and for what the value
|
|
59052
|
-
* at open is on the picker's very first open.
|
|
59053
|
-
* @param {"close"|"cancel"|"capture"} [pointerInteractionOutsideEffect="close"]
|
|
59054
|
-
* What a click outside the popup does: close and keep ("close"), close and
|
|
59055
|
-
* put back the value at open ("cancel"), or nothing at all ("capture"). The
|
|
59056
|
-
* default is what gives a popup with no confirm button its way out that
|
|
59057
|
-
* keeps — see the same section.
|
|
59058
|
-
* @param {number|string} [marginWithContainer] Minimum gap kept between the
|
|
59059
|
-
* popup and the edges of what contains it (the viewport, or the picker's own
|
|
59060
|
-
* positioned ancestor for `popupLayer="local"`). Caps the popup's size as
|
|
59061
|
-
* well as its placement, so what an expanded dialog leaves visible around
|
|
59062
|
-
* itself is set here. Defaults to `popoverSpacing` in popover mode, and to
|
|
59063
|
-
* Dialog's own 3vvw in dialog mode.
|
|
59064
|
-
*/
|
|
59065
59298
|
const Picker = createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
|
|
59066
59299
|
Picker.UI = PickerDefaultUI;
|
|
59067
59300
|
Picker.UI.Date = PickerDateUI;
|