@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/dev/jsenv_navi.js
CHANGED
|
@@ -18934,6 +18934,20 @@ const openCallout = (
|
|
|
18934
18934
|
// deduped away on the grounds that the anchor did not move.
|
|
18935
18935
|
callout.updatePosition = () =>
|
|
18936
18936
|
positioner.update(new CustomEvent(ELEMENT_SIZE_CHANGE));
|
|
18937
|
+
// Content that sizes itself once on screen (a shrink-wrapped BadgeList) does
|
|
18938
|
+
// so after the callout measured it. The size observer positionCallout puts
|
|
18939
|
+
// on the message sees the change, but answers a frame later (see observeSize
|
|
18940
|
+
// in @jsenv/dom for why); placed again on the spot, the first painted frame
|
|
18941
|
+
// is the right one. Stopped here, since a popup further up holds the callout
|
|
18942
|
+
// out of flow and does not resize with it.
|
|
18943
|
+
const onContentSizeChange = (sizeChangeEvent) => {
|
|
18944
|
+
sizeChangeEvent.stopPropagation();
|
|
18945
|
+
callout.updatePosition();
|
|
18946
|
+
};
|
|
18947
|
+
calloutElement.addEventListener("navi_size_change", onContentSizeChange);
|
|
18948
|
+
addTeardown(() => {
|
|
18949
|
+
calloutElement.removeEventListener("navi_size_change", onContentSizeChange);
|
|
18950
|
+
});
|
|
18937
18951
|
|
|
18938
18952
|
return callout;
|
|
18939
18953
|
};
|
|
@@ -43943,6 +43957,7 @@ const useRenderScope = (init, update) => {
|
|
|
43943
43957
|
* registerChild(child): void; // Called on child mount
|
|
43944
43958
|
* onChildUIAction(child, e, { stateChanged: boolean }): void; // Called when a child fires a UI action
|
|
43945
43959
|
* unregisterChild(child): void; // Called on child unmount
|
|
43960
|
+
* collectsChild(child): boolean; // Whether registerChild(child) would put it in a value, here or above
|
|
43946
43961
|
* props: Object;
|
|
43947
43962
|
* ref: Ref; // Used to dispatch DOM events
|
|
43948
43963
|
* getManagedControls(): UIStateController[]; // Returns controls whose validity is managed by this controller
|
|
@@ -44667,6 +44682,10 @@ const useUIStateController = (
|
|
|
44667
44682
|
unregisterChild: (childUIStateController) => {
|
|
44668
44683
|
s.parentUIStateController?.unregisterChild(childUIStateController);
|
|
44669
44684
|
},
|
|
44685
|
+
collectsChild: (childUIStateController) =>
|
|
44686
|
+
Boolean(
|
|
44687
|
+
s.parentUIStateController?.collectsChild(childUIStateController),
|
|
44688
|
+
),
|
|
44670
44689
|
onChildUIAction: (childUIStateController, e, options) => {
|
|
44671
44690
|
s.parentUIStateController?.onChildUIAction(
|
|
44672
44691
|
childUIStateController,
|
|
@@ -45053,10 +45072,13 @@ const readNamedChildUIStates = (children, { warnNameless }) => {
|
|
|
45053
45072
|
// Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
|
|
45054
45073
|
// distributeChildUIState. If neither a default nor an explicit impl is found for a
|
|
45055
45074
|
// group, creation throws so the caller knows it must supply them.
|
|
45075
|
+
// The radio/checkbox filters read `props.type`, which controlHostProps copies:
|
|
45076
|
+
// a child is also asked about while rendering, before its first render has
|
|
45077
|
+
// built controlHostProps (see collectsChild).
|
|
45056
45078
|
const GROUP_DEFAULTS = {
|
|
45057
45079
|
radio_group: {
|
|
45058
45080
|
childControlFilter: (child) =>
|
|
45059
|
-
child.controlType === "input" && child.
|
|
45081
|
+
child.controlType === "input" && child.props?.type === "radio",
|
|
45060
45082
|
aggregateChildStates: keptOrFirstDefinedChildUIState,
|
|
45061
45083
|
distributeChildUIState: (newUIState, childUIStateController) => {
|
|
45062
45084
|
const childSelected = childUIStateController.props.value === newUIState;
|
|
@@ -45068,8 +45090,7 @@ const GROUP_DEFAULTS = {
|
|
|
45068
45090
|
},
|
|
45069
45091
|
checkbox_group: {
|
|
45070
45092
|
childControlFilter: (child) =>
|
|
45071
|
-
child.controlType === "input" &&
|
|
45072
|
-
child.controlHostProps?.type === "checkbox",
|
|
45093
|
+
child.controlType === "input" && child.props?.type === "checkbox",
|
|
45073
45094
|
aggregateChildStates: keptAndCheckedChildUIStates,
|
|
45074
45095
|
distributeChildUIState: (newUIState, childUIStateController) => {
|
|
45075
45096
|
const childSelected =
|
|
@@ -45614,6 +45635,9 @@ const useUIGroupStateController = (
|
|
|
45614
45635
|
|
|
45615
45636
|
const controller = {
|
|
45616
45637
|
controlType,
|
|
45638
|
+
// Published from the first render, like a leaf's: a group asks its
|
|
45639
|
+
// parent whether anyone listens before its first update.
|
|
45640
|
+
parentUIStateController,
|
|
45617
45641
|
id,
|
|
45618
45642
|
name,
|
|
45619
45643
|
value,
|
|
@@ -45837,6 +45861,18 @@ const useUIGroupStateController = (
|
|
|
45837
45861
|
notifyExternal: "silent",
|
|
45838
45862
|
});
|
|
45839
45863
|
},
|
|
45864
|
+
// registerChild above, asked ahead: monitored here, handed up, or
|
|
45865
|
+
// captured and dropped.
|
|
45866
|
+
collectsChild: (childUIStateController) => {
|
|
45867
|
+
if (isMonitoringChild(childUIStateController)) {
|
|
45868
|
+
return true;
|
|
45869
|
+
}
|
|
45870
|
+
const currentParent = s.parentUIStateController;
|
|
45871
|
+
if (allowCapture || !currentParent) {
|
|
45872
|
+
return false;
|
|
45873
|
+
}
|
|
45874
|
+
return currentParent.collectsChild(childUIStateController);
|
|
45875
|
+
},
|
|
45840
45876
|
onChildUIAction: (
|
|
45841
45877
|
childUIStateController,
|
|
45842
45878
|
e,
|
|
@@ -46314,6 +46350,9 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
46314
46350
|
s.realUIStateController.facadeChild = null;
|
|
46315
46351
|
}
|
|
46316
46352
|
},
|
|
46353
|
+
// A refused child is not handed further up: the popup's controls
|
|
46354
|
+
// answer to the picker or to nobody.
|
|
46355
|
+
collectsChild: (child) => canRegisterAsFacadeChild(child),
|
|
46317
46356
|
getManagedControls: () => {
|
|
46318
46357
|
const child = firstChildControllerRef.current;
|
|
46319
46358
|
if (!child) {
|
|
@@ -46934,8 +46973,6 @@ const useControlProps = (props, {
|
|
|
46934
46973
|
const controlInfo = createControlInfo(props, {
|
|
46935
46974
|
controlType
|
|
46936
46975
|
});
|
|
46937
|
-
const readOnlyUncontrolled = useReadOnlyUncontrolled(props, controlInfo);
|
|
46938
|
-
controlInfo.readOnlyUncontrolled = readOnlyUncontrolled;
|
|
46939
46976
|
const uiStateController = useUIStateController(props, {
|
|
46940
46977
|
controlInfo,
|
|
46941
46978
|
syncDomState,
|
|
@@ -46943,6 +46980,9 @@ const useControlProps = (props, {
|
|
|
46943
46980
|
persists,
|
|
46944
46981
|
uiActionInternal
|
|
46945
46982
|
});
|
|
46983
|
+
// After the controller: whether a group listens is asked of the parent it
|
|
46984
|
+
// registers into. Read by useInteractiveProps below.
|
|
46985
|
+
controlInfo.readOnlyUncontrolled = useReadOnlyUncontrolled(props, controlInfo, uiStateController);
|
|
46946
46986
|
// What the control draws is what it holds by itself, and this read is what
|
|
46947
46987
|
// re-renders it when that changes. A button inheriting the value of the
|
|
46948
46988
|
// control around it (a submit in a form, see ownUIStateSignal) draws none of
|
|
@@ -47948,16 +47988,21 @@ const INPUT_TYPE_SUPPORTING_READONLY_SET = new Set(["text", "date", "datetime-lo
|
|
|
47948
47988
|
// warning below so a group can ask the same question a single control does: a
|
|
47949
47989
|
// control with a `value` and nobody listening cannot be changed by hand, and
|
|
47950
47990
|
// that is true whatever the control is.
|
|
47951
|
-
|
|
47991
|
+
// The group is asked, not assumed from a controller being above: a control in
|
|
47992
|
+
// between may only pass registration along (a <Details> holds none of the
|
|
47993
|
+
// values it folds), and a group may hand the control further up or drop it.
|
|
47994
|
+
const useIsControlListenedTo = (props, uiStateController) => {
|
|
47952
47995
|
const isProxy = Boolean(props["navi-control-proxy-for"]);
|
|
47953
47996
|
const formContext = useContext(FormContext);
|
|
47954
|
-
const
|
|
47997
|
+
const {
|
|
47998
|
+
parentUIStateController
|
|
47999
|
+
} = uiStateController;
|
|
47955
48000
|
return Boolean(props.signal ||
|
|
47956
48001
|
// a bound signal is written back on uiAction → interactive
|
|
47957
|
-
props.uiAction || props.action || formContext || parentUIStateController || isProxy || props.command);
|
|
48002
|
+
props.uiAction || props.action || formContext || parentUIStateController?.collectsChild(uiStateController) || isProxy || props.command);
|
|
47958
48003
|
};
|
|
47959
|
-
const useReadOnlyUncontrolled = (props, controlInfo) => {
|
|
47960
|
-
const listenedTo = useIsControlListenedTo(props);
|
|
48004
|
+
const useReadOnlyUncontrolled = (props, controlInfo, uiStateController) => {
|
|
48005
|
+
const listenedTo = useIsControlListenedTo(props, uiStateController);
|
|
47961
48006
|
const warnedRef = useRef(false);
|
|
47962
48007
|
if (!controlInfo.hasStateProp) {
|
|
47963
48008
|
return false;
|
|
@@ -48023,9 +48068,9 @@ const useControlgroupProps = (props, {
|
|
|
48023
48068
|
const [boundAction] = useActionBoundToOneParam(action, uiGroupStateController.uiStateSignal);
|
|
48024
48069
|
// Mirror single-input behaviour: a controlled value with nobody listening
|
|
48025
48070
|
// makes the group read-only so children don't appear interactive when they
|
|
48026
|
-
// can't change. A form or a group
|
|
48027
|
-
// what will send the value and hand a new one back.
|
|
48028
|
-
const listenedTo = useIsControlListenedTo(props);
|
|
48071
|
+
// can't change. A form or a group collecting it IS someone listening — that
|
|
48072
|
+
// is what will send the value and hand a new one back.
|
|
48073
|
+
const listenedTo = useIsControlListenedTo(props, uiGroupStateController);
|
|
48029
48074
|
const implicitReadOnlyWarnedRef = useRef(false);
|
|
48030
48075
|
const implicitReadOnly = uiGroupStateController.hasValueProp && !listenedTo;
|
|
48031
48076
|
if (implicitReadOnly && !props.readOnly) {
|
|
@@ -71552,6 +71597,15 @@ const useDialogProps = props => {
|
|
|
71552
71597
|
freezeSize(dialogEl);
|
|
71553
71598
|
}
|
|
71554
71599
|
rectEffect.observeSize(dialogEl);
|
|
71600
|
+
// Content that sizes itself once on screen (a shrink-wrapped BadgeList)
|
|
71601
|
+
// says so, after the placement read it: placed again on the spot, where
|
|
71602
|
+
// observeSize answers a frame late. Stopped here, since a popup further up
|
|
71603
|
+
// holds this one out of flow and does not resize with it.
|
|
71604
|
+
const onContentSizeChange = sizeChangeEvent => {
|
|
71605
|
+
sizeChangeEvent.stopPropagation();
|
|
71606
|
+
rectEffect.check(new CustomEvent(ELEMENT_SIZE_CHANGE));
|
|
71607
|
+
};
|
|
71608
|
+
dialogEl.addEventListener("navi_size_change", onContentSizeChange);
|
|
71555
71609
|
// Exposed for the placement-props effect below, which needs to re-place an
|
|
71556
71610
|
// already-open dialog.
|
|
71557
71611
|
repositionRef.current = repositionEvent => {
|
|
@@ -71565,6 +71619,7 @@ const useDialogProps = props => {
|
|
|
71565
71619
|
};
|
|
71566
71620
|
addCleanup(() => {
|
|
71567
71621
|
repositionRef.current = null;
|
|
71622
|
+
dialogEl.removeEventListener("navi_size_change", onContentSizeChange);
|
|
71568
71623
|
rectEffect.disconnect();
|
|
71569
71624
|
});
|
|
71570
71625
|
// A descendant anchored to something inside this dialog (a Callout, a
|
|
@@ -73395,6 +73450,18 @@ const usePopoverProps = props => {
|
|
|
73395
73450
|
// while open (e.g. an expand/collapse toggle inside it) — not just when
|
|
73396
73451
|
// the anchor itself moves/resizes/re-anchors.
|
|
73397
73452
|
rectEffect.observeSize(popoverEl);
|
|
73453
|
+
// Content that sizes itself once on screen (a shrink-wrapped BadgeList)
|
|
73454
|
+
// says so, after the placement read it: placed again on the spot, where
|
|
73455
|
+
// observeSize answers a frame late. Stopped here, since a popup further up
|
|
73456
|
+
// holds this one out of flow and does not resize with it.
|
|
73457
|
+
const onContentSizeChange = sizeChangeEvent => {
|
|
73458
|
+
sizeChangeEvent.stopPropagation();
|
|
73459
|
+
rectEffect.check(new CustomEvent(ELEMENT_SIZE_CHANGE));
|
|
73460
|
+
};
|
|
73461
|
+
popoverEl.addEventListener("navi_size_change", onContentSizeChange);
|
|
73462
|
+
addCleanup(() => {
|
|
73463
|
+
popoverEl.removeEventListener("navi_size_change", onContentSizeChange);
|
|
73464
|
+
});
|
|
73398
73465
|
// Exposed for the placement-props effect above, which needs to re-place an
|
|
73399
73466
|
// already-open popover.
|
|
73400
73467
|
repositionRef.current = repositionEvent => {
|
|
@@ -74449,8 +74516,15 @@ const PickerCustom = props => {
|
|
|
74449
74516
|
// moment both exist. A picker that picks nothing holds none: its popup's
|
|
74450
74517
|
// controls are never adopted (see canRegisterAsFacadeChild).
|
|
74451
74518
|
const valueInPopup = !props.picksNothing && !isControlValueGivenByProps(props);
|
|
74519
|
+
// Asked of the chain, not read off the context: a control above may only
|
|
74520
|
+
// pass registration along (a <Details> holds none of the values it folds),
|
|
74521
|
+
// and a group may hand a picker further up or drop it. "picker" is what the
|
|
74522
|
+
// picker's own controller registers as (see picker.jsx).
|
|
74452
74523
|
const parentController = useContext(ParentUIStateControllerContext);
|
|
74453
|
-
const answersToGroup =
|
|
74524
|
+
const answersToGroup = !props.standalone && Boolean(parentController?.collectsChild({
|
|
74525
|
+
controlType: "picker",
|
|
74526
|
+
props
|
|
74527
|
+
}));
|
|
74454
74528
|
// Same narrow-container/maxWidth-compact heuristic Popup itself uses (see
|
|
74455
74529
|
// popup_mode.jsx's own useResolvedPopupMode) — frozen for the lifetime of an opening
|
|
74456
74530
|
// (computed when closed, stable while open, so a screen resize mid-session
|
|
@@ -85041,39 +85115,29 @@ const BadgeListShrinkWrap = ({
|
|
|
85041
85115
|
const registry = useBadgeRegistry(children, max !== undefined || fallback !== undefined);
|
|
85042
85116
|
const measureRef = useRef();
|
|
85043
85117
|
const visibleRef = useRef();
|
|
85118
|
+
|
|
85119
|
+
// In a closed popup the rows have no size to read: the list is measured when
|
|
85120
|
+
// it comes on screen, before the frame is painted.
|
|
85121
|
+
useDisplayedLayoutEffect(visibleRef, () => {
|
|
85122
|
+
shrinkWrapToWidestRow(visibleRef.current, measureRef.current);
|
|
85123
|
+
}, [children]);
|
|
85044
85124
|
useLayoutEffect(() => {
|
|
85045
85125
|
const measureEl = measureRef.current;
|
|
85046
85126
|
const visibleEl = visibleRef.current;
|
|
85047
85127
|
if (!measureEl || !visibleEl) {
|
|
85048
85128
|
return undefined;
|
|
85049
85129
|
}
|
|
85050
|
-
|
|
85051
|
-
let rafId;
|
|
85052
|
-
const measure = () => {
|
|
85053
|
-
visibleEl.style.width = "";
|
|
85054
|
-
// Clone the already-rendered DOM nodes instead of letting React/Preact
|
|
85055
|
-
// render the children a second time into the ghost: re-rendering would
|
|
85056
|
-
// instantiate Badge/Badge.Button a second time, double-registering their
|
|
85057
|
-
// controllers (and any other mount side effect) under the same id.
|
|
85058
|
-
measureEl.replaceChildren(...Array.from(visibleEl.children, child => child.cloneNode(true)));
|
|
85059
|
-
const optimalWidth = measureWidestChildRow(measureEl);
|
|
85060
|
-
if (optimalWidth !== null) {
|
|
85061
|
-
visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
|
|
85062
|
-
}
|
|
85063
|
-
};
|
|
85064
|
-
|
|
85065
|
-
// A single badge is a single row, and a single row is already the widest
|
|
85066
|
-
// one: there is nothing to narrow, and no width the list could be given
|
|
85067
|
-
// that would change that.
|
|
85130
|
+
// A single row whatever room it is given: nothing a resize could narrow.
|
|
85068
85131
|
if (visibleEl.children.length < 2) {
|
|
85069
|
-
visibleEl.style.width = "";
|
|
85070
|
-
measureEl.replaceChildren();
|
|
85071
85132
|
return undefined;
|
|
85072
85133
|
}
|
|
85073
|
-
|
|
85134
|
+
let observer;
|
|
85135
|
+
let rafId;
|
|
85074
85136
|
const onResize = () => {
|
|
85075
85137
|
cancelAnimationFrame(rafId);
|
|
85076
|
-
rafId = requestAnimationFrame(
|
|
85138
|
+
rafId = requestAnimationFrame(() => {
|
|
85139
|
+
shrinkWrapToWidestRow(visibleEl, measureEl);
|
|
85140
|
+
});
|
|
85077
85141
|
};
|
|
85078
85142
|
const outerParent = measureEl.parentElement?.parentElement;
|
|
85079
85143
|
if (outerParent) {
|
|
@@ -85119,6 +85183,40 @@ const BadgeListShrinkWrap = ({
|
|
|
85119
85183
|
})
|
|
85120
85184
|
);
|
|
85121
85185
|
};
|
|
85186
|
+
|
|
85187
|
+
// Narrows the list to its widest row, read from clones of its badges in the
|
|
85188
|
+
// ghost.
|
|
85189
|
+
//
|
|
85190
|
+
// What holds the list may have sized itself around the width it had before —
|
|
85191
|
+
// a callout pins its body to the width it measured — so a change is announced
|
|
85192
|
+
// with a bubbling navi_size_change for it to measure again, in the same frame.
|
|
85193
|
+
const shrinkWrapToWidestRow = (visibleEl, measureEl) => {
|
|
85194
|
+
// Not drawn (back in a closed popup): no row to read. The width is kept for
|
|
85195
|
+
// the next opening, which measures again before painting.
|
|
85196
|
+
if (visibleEl.getClientRects().length === 0) {
|
|
85197
|
+
return;
|
|
85198
|
+
}
|
|
85199
|
+
const widthBefore = visibleEl.style.width;
|
|
85200
|
+
visibleEl.style.width = "";
|
|
85201
|
+
if (visibleEl.children.length < 2) {
|
|
85202
|
+
// A single badge is a single row, and a single row is already the widest
|
|
85203
|
+
// one: there is nothing to narrow.
|
|
85204
|
+
measureEl.replaceChildren();
|
|
85205
|
+
} else {
|
|
85206
|
+
// Clone the already-rendered DOM nodes instead of letting React/Preact
|
|
85207
|
+
// render the children a second time into the ghost: re-rendering would
|
|
85208
|
+
// instantiate Badge/Badge.Button a second time, double-registering their
|
|
85209
|
+
// controllers (and any other mount side effect) under the same id.
|
|
85210
|
+
measureEl.replaceChildren(...Array.from(visibleEl.children, child => child.cloneNode(true)));
|
|
85211
|
+
const optimalWidth = measureWidestChildRow(measureEl);
|
|
85212
|
+
if (optimalWidth !== null) {
|
|
85213
|
+
visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
|
|
85214
|
+
}
|
|
85215
|
+
}
|
|
85216
|
+
if (visibleEl.style.width !== widthBefore) {
|
|
85217
|
+
dispatchPublicCustomEvent(visibleEl, "navi_size_change");
|
|
85218
|
+
}
|
|
85219
|
+
};
|
|
85122
85220
|
const BadgeListMaxLines = ({
|
|
85123
85221
|
fallback,
|
|
85124
85222
|
children,
|