@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.
@@ -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.controlHostProps?.type === "radio",
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
- const useIsControlListenedTo = props => {
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 parentUIStateController = useContext(ParentUIStateControllerContext);
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 around it IS someone listening — that is
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 = Boolean(parentController) && !props.standalone;
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
- let observer;
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
- measure();
83533
+ let observer;
83534
+ let rafId;
83473
83535
  const onResize = () => {
83474
83536
  cancelAnimationFrame(rafId);
83475
- rafId = requestAnimationFrame(measure);
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,