@jsenv/navi 0.29.472 → 0.29.474
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/dev/jsenv_navi.js +134 -36
- package/dist/dev/jsenv_navi.js.map +8 -8
- package/dist/jsenv_navi.js +134 -36
- package/dist/jsenv_navi.js.map +8 -8
- package/docs/control_value.md +3 -2
- package/docs/popup_open.md +10 -9
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -18307,6 +18307,20 @@ const openCallout = (
|
|
|
18307
18307
|
// deduped away on the grounds that the anchor did not move.
|
|
18308
18308
|
callout.updatePosition = () =>
|
|
18309
18309
|
positioner.update(new CustomEvent(ELEMENT_SIZE_CHANGE));
|
|
18310
|
+
// Content that sizes itself once on screen (a shrink-wrapped BadgeList) does
|
|
18311
|
+
// so after the callout measured it. The size observer positionCallout puts
|
|
18312
|
+
// on the message sees the change, but answers a frame later (see observeSize
|
|
18313
|
+
// in @jsenv/dom for why); placed again on the spot, the first painted frame
|
|
18314
|
+
// is the right one. Stopped here, since a popup further up holds the callout
|
|
18315
|
+
// out of flow and does not resize with it.
|
|
18316
|
+
const onContentSizeChange = (sizeChangeEvent) => {
|
|
18317
|
+
sizeChangeEvent.stopPropagation();
|
|
18318
|
+
callout.updatePosition();
|
|
18319
|
+
};
|
|
18320
|
+
calloutElement.addEventListener("navi_size_change", onContentSizeChange);
|
|
18321
|
+
addTeardown(() => {
|
|
18322
|
+
calloutElement.removeEventListener("navi_size_change", onContentSizeChange);
|
|
18323
|
+
});
|
|
18310
18324
|
|
|
18311
18325
|
return callout;
|
|
18312
18326
|
};
|
|
@@ -42894,6 +42908,7 @@ const useRenderScope = (init, update) => {
|
|
|
42894
42908
|
* registerChild(child): void; // Called on child mount
|
|
42895
42909
|
* onChildUIAction(child, e, { stateChanged: boolean }): void; // Called when a child fires a UI action
|
|
42896
42910
|
* unregisterChild(child): void; // Called on child unmount
|
|
42911
|
+
* collectsChild(child): boolean; // Whether registerChild(child) would put it in a value, here or above
|
|
42897
42912
|
* props: Object;
|
|
42898
42913
|
* ref: Ref; // Used to dispatch DOM events
|
|
42899
42914
|
* getManagedControls(): UIStateController[]; // Returns controls whose validity is managed by this controller
|
|
@@ -43596,6 +43611,10 @@ const useUIStateController = (
|
|
|
43596
43611
|
unregisterChild: (childUIStateController) => {
|
|
43597
43612
|
s.parentUIStateController?.unregisterChild(childUIStateController);
|
|
43598
43613
|
},
|
|
43614
|
+
collectsChild: (childUIStateController) =>
|
|
43615
|
+
Boolean(
|
|
43616
|
+
s.parentUIStateController?.collectsChild(childUIStateController),
|
|
43617
|
+
),
|
|
43599
43618
|
onChildUIAction: (childUIStateController, e, options) => {
|
|
43600
43619
|
s.parentUIStateController?.onChildUIAction(
|
|
43601
43620
|
childUIStateController,
|
|
@@ -43982,10 +44001,13 @@ const readNamedChildUIStates = (children, { warnNameless }) => {
|
|
|
43982
44001
|
// Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
|
|
43983
44002
|
// distributeChildUIState. If neither a default nor an explicit impl is found for a
|
|
43984
44003
|
// group, creation throws so the caller knows it must supply them.
|
|
44004
|
+
// The radio/checkbox filters read `props.type`, which controlHostProps copies:
|
|
44005
|
+
// a child is also asked about while rendering, before its first render has
|
|
44006
|
+
// built controlHostProps (see collectsChild).
|
|
43985
44007
|
const GROUP_DEFAULTS = {
|
|
43986
44008
|
radio_group: {
|
|
43987
44009
|
childControlFilter: (child) =>
|
|
43988
|
-
child.controlType === "input" && child.
|
|
44010
|
+
child.controlType === "input" && child.props?.type === "radio",
|
|
43989
44011
|
aggregateChildStates: keptOrFirstDefinedChildUIState,
|
|
43990
44012
|
distributeChildUIState: (newUIState, childUIStateController) => {
|
|
43991
44013
|
const childSelected = childUIStateController.props.value === newUIState;
|
|
@@ -43997,8 +44019,7 @@ const GROUP_DEFAULTS = {
|
|
|
43997
44019
|
},
|
|
43998
44020
|
checkbox_group: {
|
|
43999
44021
|
childControlFilter: (child) =>
|
|
44000
|
-
child.controlType === "input" &&
|
|
44001
|
-
child.controlHostProps?.type === "checkbox",
|
|
44022
|
+
child.controlType === "input" && child.props?.type === "checkbox",
|
|
44002
44023
|
aggregateChildStates: keptAndCheckedChildUIStates,
|
|
44003
44024
|
distributeChildUIState: (newUIState, childUIStateController) => {
|
|
44004
44025
|
const childSelected =
|
|
@@ -44533,6 +44554,9 @@ const useUIGroupStateController = (
|
|
|
44533
44554
|
|
|
44534
44555
|
const controller = {
|
|
44535
44556
|
controlType,
|
|
44557
|
+
// Published from the first render, like a leaf's: a group asks its
|
|
44558
|
+
// parent whether anyone listens before its first update.
|
|
44559
|
+
parentUIStateController,
|
|
44536
44560
|
id,
|
|
44537
44561
|
name,
|
|
44538
44562
|
value,
|
|
@@ -44756,6 +44780,18 @@ const useUIGroupStateController = (
|
|
|
44756
44780
|
notifyExternal: "silent",
|
|
44757
44781
|
});
|
|
44758
44782
|
},
|
|
44783
|
+
// registerChild above, asked ahead: monitored here, handed up, or
|
|
44784
|
+
// captured and dropped.
|
|
44785
|
+
collectsChild: (childUIStateController) => {
|
|
44786
|
+
if (isMonitoringChild(childUIStateController)) {
|
|
44787
|
+
return true;
|
|
44788
|
+
}
|
|
44789
|
+
const currentParent = s.parentUIStateController;
|
|
44790
|
+
if (allowCapture || !currentParent) {
|
|
44791
|
+
return false;
|
|
44792
|
+
}
|
|
44793
|
+
return currentParent.collectsChild(childUIStateController);
|
|
44794
|
+
},
|
|
44759
44795
|
onChildUIAction: (
|
|
44760
44796
|
childUIStateController,
|
|
44761
44797
|
e,
|
|
@@ -45228,6 +45264,9 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
45228
45264
|
s.realUIStateController.facadeChild = null;
|
|
45229
45265
|
}
|
|
45230
45266
|
},
|
|
45267
|
+
// A refused child is not handed further up: the popup's controls
|
|
45268
|
+
// answer to the picker or to nobody.
|
|
45269
|
+
collectsChild: (child) => canRegisterAsFacadeChild(child),
|
|
45231
45270
|
getManagedControls: () => {
|
|
45232
45271
|
const child = firstChildControllerRef.current;
|
|
45233
45272
|
if (!child) {
|
|
@@ -45769,8 +45808,6 @@ const useControlProps = (props, {
|
|
|
45769
45808
|
const controlInfo = createControlInfo(props, {
|
|
45770
45809
|
controlType
|
|
45771
45810
|
});
|
|
45772
|
-
const readOnlyUncontrolled = useReadOnlyUncontrolled(props, controlInfo);
|
|
45773
|
-
controlInfo.readOnlyUncontrolled = readOnlyUncontrolled;
|
|
45774
45811
|
const uiStateController = useUIStateController(props, {
|
|
45775
45812
|
controlInfo,
|
|
45776
45813
|
syncDomState,
|
|
@@ -45778,6 +45815,9 @@ const useControlProps = (props, {
|
|
|
45778
45815
|
persists,
|
|
45779
45816
|
uiActionInternal
|
|
45780
45817
|
});
|
|
45818
|
+
// After the controller: whether a group listens is asked of the parent it
|
|
45819
|
+
// registers into. Read by useInteractiveProps below.
|
|
45820
|
+
controlInfo.readOnlyUncontrolled = useReadOnlyUncontrolled(props, controlInfo, uiStateController);
|
|
45781
45821
|
// What the control draws is what it holds by itself, and this read is what
|
|
45782
45822
|
// re-renders it when that changes. A button inheriting the value of the
|
|
45783
45823
|
// control around it (a submit in a form, see ownUIStateSignal) draws none of
|
|
@@ -46773,16 +46813,21 @@ const INPUT_TYPE_SUPPORTING_READONLY_SET = new Set(["text", "date", "datetime-lo
|
|
|
46773
46813
|
// warning below so a group can ask the same question a single control does: a
|
|
46774
46814
|
// control with a `value` and nobody listening cannot be changed by hand, and
|
|
46775
46815
|
// that is true whatever the control is.
|
|
46776
|
-
|
|
46816
|
+
// The group is asked, not assumed from a controller being above: a control in
|
|
46817
|
+
// between may only pass registration along (a <Details> holds none of the
|
|
46818
|
+
// values it folds), and a group may hand the control further up or drop it.
|
|
46819
|
+
const useIsControlListenedTo = (props, uiStateController) => {
|
|
46777
46820
|
const isProxy = Boolean(props["navi-control-proxy-for"]);
|
|
46778
46821
|
const formContext = useContext(FormContext);
|
|
46779
|
-
const
|
|
46822
|
+
const {
|
|
46823
|
+
parentUIStateController
|
|
46824
|
+
} = uiStateController;
|
|
46780
46825
|
return Boolean(props.signal ||
|
|
46781
46826
|
// a bound signal is written back on uiAction → interactive
|
|
46782
|
-
props.uiAction || props.action || formContext || parentUIStateController || isProxy || props.command);
|
|
46827
|
+
props.uiAction || props.action || formContext || parentUIStateController?.collectsChild(uiStateController) || isProxy || props.command);
|
|
46783
46828
|
};
|
|
46784
|
-
const useReadOnlyUncontrolled = (props, controlInfo) => {
|
|
46785
|
-
const listenedTo = useIsControlListenedTo(props);
|
|
46829
|
+
const useReadOnlyUncontrolled = (props, controlInfo, uiStateController) => {
|
|
46830
|
+
const listenedTo = useIsControlListenedTo(props, uiStateController);
|
|
46786
46831
|
useRef(false);
|
|
46787
46832
|
if (!controlInfo.hasStateProp) {
|
|
46788
46833
|
return false;
|
|
@@ -46839,9 +46884,9 @@ const useControlgroupProps = (props, {
|
|
|
46839
46884
|
const [boundAction] = useActionBoundToOneParam(action, uiGroupStateController.uiStateSignal);
|
|
46840
46885
|
// Mirror single-input behaviour: a controlled value with nobody listening
|
|
46841
46886
|
// makes the group read-only so children don't appear interactive when they
|
|
46842
|
-
// can't change. A form or a group
|
|
46843
|
-
// what will send the value and hand a new one back.
|
|
46844
|
-
const listenedTo = useIsControlListenedTo(props);
|
|
46887
|
+
// can't change. A form or a group collecting it IS someone listening — that
|
|
46888
|
+
// is what will send the value and hand a new one back.
|
|
46889
|
+
const listenedTo = useIsControlListenedTo(props, uiGroupStateController);
|
|
46845
46890
|
useRef(false);
|
|
46846
46891
|
const implicitReadOnly = uiGroupStateController.hasValueProp && !listenedTo;
|
|
46847
46892
|
if (implicitReadOnly && !props.readOnly) {
|
|
@@ -70179,6 +70224,15 @@ const useDialogProps = props => {
|
|
|
70179
70224
|
freezeSize(dialogEl);
|
|
70180
70225
|
}
|
|
70181
70226
|
rectEffect.observeSize(dialogEl);
|
|
70227
|
+
// Content that sizes itself once on screen (a shrink-wrapped BadgeList)
|
|
70228
|
+
// says so, after the placement read it: placed again on the spot, where
|
|
70229
|
+
// observeSize answers a frame late. Stopped here, since a popup further up
|
|
70230
|
+
// holds this one out of flow and does not resize with it.
|
|
70231
|
+
const onContentSizeChange = sizeChangeEvent => {
|
|
70232
|
+
sizeChangeEvent.stopPropagation();
|
|
70233
|
+
rectEffect.check(new CustomEvent(ELEMENT_SIZE_CHANGE));
|
|
70234
|
+
};
|
|
70235
|
+
dialogEl.addEventListener("navi_size_change", onContentSizeChange);
|
|
70182
70236
|
// Exposed for the placement-props effect below, which needs to re-place an
|
|
70183
70237
|
// already-open dialog.
|
|
70184
70238
|
repositionRef.current = repositionEvent => {
|
|
@@ -70192,6 +70246,7 @@ const useDialogProps = props => {
|
|
|
70192
70246
|
};
|
|
70193
70247
|
addCleanup(() => {
|
|
70194
70248
|
repositionRef.current = null;
|
|
70249
|
+
dialogEl.removeEventListener("navi_size_change", onContentSizeChange);
|
|
70195
70250
|
rectEffect.disconnect();
|
|
70196
70251
|
});
|
|
70197
70252
|
// A descendant anchored to something inside this dialog (a Callout, a
|
|
@@ -71984,6 +72039,18 @@ const usePopoverProps = props => {
|
|
|
71984
72039
|
// while open (e.g. an expand/collapse toggle inside it) — not just when
|
|
71985
72040
|
// the anchor itself moves/resizes/re-anchors.
|
|
71986
72041
|
rectEffect.observeSize(popoverEl);
|
|
72042
|
+
// Content that sizes itself once on screen (a shrink-wrapped BadgeList)
|
|
72043
|
+
// says so, after the placement read it: placed again on the spot, where
|
|
72044
|
+
// observeSize answers a frame late. Stopped here, since a popup further up
|
|
72045
|
+
// holds this one out of flow and does not resize with it.
|
|
72046
|
+
const onContentSizeChange = sizeChangeEvent => {
|
|
72047
|
+
sizeChangeEvent.stopPropagation();
|
|
72048
|
+
rectEffect.check(new CustomEvent(ELEMENT_SIZE_CHANGE));
|
|
72049
|
+
};
|
|
72050
|
+
popoverEl.addEventListener("navi_size_change", onContentSizeChange);
|
|
72051
|
+
addCleanup(() => {
|
|
72052
|
+
popoverEl.removeEventListener("navi_size_change", onContentSizeChange);
|
|
72053
|
+
});
|
|
71987
72054
|
// Exposed for the placement-props effect above, which needs to re-place an
|
|
71988
72055
|
// already-open popover.
|
|
71989
72056
|
repositionRef.current = repositionEvent => {
|
|
@@ -73037,8 +73104,15 @@ const PickerCustom = props => {
|
|
|
73037
73104
|
// moment both exist. A picker that picks nothing holds none: its popup's
|
|
73038
73105
|
// controls are never adopted (see canRegisterAsFacadeChild).
|
|
73039
73106
|
const valueInPopup = !props.picksNothing && !isControlValueGivenByProps(props);
|
|
73107
|
+
// Asked of the chain, not read off the context: a control above may only
|
|
73108
|
+
// pass registration along (a <Details> holds none of the values it folds),
|
|
73109
|
+
// and a group may hand a picker further up or drop it. "picker" is what the
|
|
73110
|
+
// picker's own controller registers as (see picker.jsx).
|
|
73040
73111
|
const parentController = useContext(ParentUIStateControllerContext);
|
|
73041
|
-
const answersToGroup =
|
|
73112
|
+
const answersToGroup = !props.standalone && Boolean(parentController?.collectsChild({
|
|
73113
|
+
controlType: "picker",
|
|
73114
|
+
props
|
|
73115
|
+
}));
|
|
73042
73116
|
// Same narrow-container/maxWidth-compact heuristic Popup itself uses (see
|
|
73043
73117
|
// popup_mode.jsx's own useResolvedPopupMode) — frozen for the lifetime of an opening
|
|
73044
73118
|
// (computed when closed, stable while open, so a screen resize mid-session
|
|
@@ -83440,39 +83514,29 @@ const BadgeListShrinkWrap = ({
|
|
|
83440
83514
|
const registry = useBadgeRegistry(children, max !== undefined || fallback !== undefined);
|
|
83441
83515
|
const measureRef = useRef();
|
|
83442
83516
|
const visibleRef = useRef();
|
|
83517
|
+
|
|
83518
|
+
// In a closed popup the rows have no size to read: the list is measured when
|
|
83519
|
+
// it comes on screen, before the frame is painted.
|
|
83520
|
+
useDisplayedLayoutEffect(visibleRef, () => {
|
|
83521
|
+
shrinkWrapToWidestRow(visibleRef.current, measureRef.current);
|
|
83522
|
+
}, [children]);
|
|
83443
83523
|
useLayoutEffect(() => {
|
|
83444
83524
|
const measureEl = measureRef.current;
|
|
83445
83525
|
const visibleEl = visibleRef.current;
|
|
83446
83526
|
if (!measureEl || !visibleEl) {
|
|
83447
83527
|
return undefined;
|
|
83448
83528
|
}
|
|
83449
|
-
|
|
83450
|
-
let rafId;
|
|
83451
|
-
const measure = () => {
|
|
83452
|
-
visibleEl.style.width = "";
|
|
83453
|
-
// Clone the already-rendered DOM nodes instead of letting React/Preact
|
|
83454
|
-
// render the children a second time into the ghost: re-rendering would
|
|
83455
|
-
// instantiate Badge/Badge.Button a second time, double-registering their
|
|
83456
|
-
// controllers (and any other mount side effect) under the same id.
|
|
83457
|
-
measureEl.replaceChildren(...Array.from(visibleEl.children, child => child.cloneNode(true)));
|
|
83458
|
-
const optimalWidth = measureWidestChildRow(measureEl);
|
|
83459
|
-
if (optimalWidth !== null) {
|
|
83460
|
-
visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
|
|
83461
|
-
}
|
|
83462
|
-
};
|
|
83463
|
-
|
|
83464
|
-
// A single badge is a single row, and a single row is already the widest
|
|
83465
|
-
// one: there is nothing to narrow, and no width the list could be given
|
|
83466
|
-
// that would change that.
|
|
83529
|
+
// A single row whatever room it is given: nothing a resize could narrow.
|
|
83467
83530
|
if (visibleEl.children.length < 2) {
|
|
83468
|
-
visibleEl.style.width = "";
|
|
83469
|
-
measureEl.replaceChildren();
|
|
83470
83531
|
return undefined;
|
|
83471
83532
|
}
|
|
83472
|
-
|
|
83533
|
+
let observer;
|
|
83534
|
+
let rafId;
|
|
83473
83535
|
const onResize = () => {
|
|
83474
83536
|
cancelAnimationFrame(rafId);
|
|
83475
|
-
rafId = requestAnimationFrame(
|
|
83537
|
+
rafId = requestAnimationFrame(() => {
|
|
83538
|
+
shrinkWrapToWidestRow(visibleEl, measureEl);
|
|
83539
|
+
});
|
|
83476
83540
|
};
|
|
83477
83541
|
const outerParent = measureEl.parentElement?.parentElement;
|
|
83478
83542
|
if (outerParent) {
|
|
@@ -83518,6 +83582,40 @@ const BadgeListShrinkWrap = ({
|
|
|
83518
83582
|
})
|
|
83519
83583
|
);
|
|
83520
83584
|
};
|
|
83585
|
+
|
|
83586
|
+
// Narrows the list to its widest row, read from clones of its badges in the
|
|
83587
|
+
// ghost.
|
|
83588
|
+
//
|
|
83589
|
+
// What holds the list may have sized itself around the width it had before —
|
|
83590
|
+
// a callout pins its body to the width it measured — so a change is announced
|
|
83591
|
+
// with a bubbling navi_size_change for it to measure again, in the same frame.
|
|
83592
|
+
const shrinkWrapToWidestRow = (visibleEl, measureEl) => {
|
|
83593
|
+
// Not drawn (back in a closed popup): no row to read. The width is kept for
|
|
83594
|
+
// the next opening, which measures again before painting.
|
|
83595
|
+
if (visibleEl.getClientRects().length === 0) {
|
|
83596
|
+
return;
|
|
83597
|
+
}
|
|
83598
|
+
const widthBefore = visibleEl.style.width;
|
|
83599
|
+
visibleEl.style.width = "";
|
|
83600
|
+
if (visibleEl.children.length < 2) {
|
|
83601
|
+
// A single badge is a single row, and a single row is already the widest
|
|
83602
|
+
// one: there is nothing to narrow.
|
|
83603
|
+
measureEl.replaceChildren();
|
|
83604
|
+
} else {
|
|
83605
|
+
// Clone the already-rendered DOM nodes instead of letting React/Preact
|
|
83606
|
+
// render the children a second time into the ghost: re-rendering would
|
|
83607
|
+
// instantiate Badge/Badge.Button a second time, double-registering their
|
|
83608
|
+
// controllers (and any other mount side effect) under the same id.
|
|
83609
|
+
measureEl.replaceChildren(...Array.from(visibleEl.children, child => child.cloneNode(true)));
|
|
83610
|
+
const optimalWidth = measureWidestChildRow(measureEl);
|
|
83611
|
+
if (optimalWidth !== null) {
|
|
83612
|
+
visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
|
|
83613
|
+
}
|
|
83614
|
+
}
|
|
83615
|
+
if (visibleEl.style.width !== widthBefore) {
|
|
83616
|
+
dispatchPublicCustomEvent(visibleEl, "navi_size_change");
|
|
83617
|
+
}
|
|
83618
|
+
};
|
|
83521
83619
|
const BadgeListMaxLines = ({
|
|
83522
83620
|
fallback,
|
|
83523
83621
|
children,
|