@jsenv/navi 0.29.305 → 0.29.306

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.
@@ -4947,6 +4947,11 @@ const radioControllersByName = new Map();
4947
4947
  // The controller id is used as key — if the controller has no id, the value is not registered.
4948
4948
  const naviJsRegistry = new Map();
4949
4949
 
4950
+ // Controllers already reported for sharing their id with another one. An id is
4951
+ // stated on every render of its control, so without this the warning below
4952
+ // would repeat for as long as both controls are on screen.
4953
+ const controllersWarned = new WeakSet();
4954
+
4950
4955
  const getUIStateControllerById = (id) => controllersById.get(id);
4951
4956
  const getRadioSiblings = (radioUIStateController) => {
4952
4957
  const siblings = radioControllersByName.get(radioUIStateController.name);
@@ -4984,10 +4989,15 @@ const onUIStateControllerCreated = (uiStateController) => {
4984
4989
  if (id) {
4985
4990
  {
4986
4991
  const existing = controllersById.get(id);
4987
- if (existing && existing !== uiStateController) {
4992
+ if (
4993
+ existing &&
4994
+ existing !== uiStateController &&
4995
+ !controllersWarned.has(uiStateController)
4996
+ ) {
4997
+ controllersWarned.add(uiStateController);
4988
4998
  console.warn(
4989
4999
  `[navi] Two controls share the same id "${id}" ("${existing.controlType}" and "${controlType}"). ` +
4990
- `Lookups by id resolve to an arbitrary one of them, and when either unmounts it deletes the registry entry, leaving the surviving control unreachable (undefined uiState, dead picker buttons). ` +
5000
+ `The id resolves to whichever of them rendered last, so the other one is unreachable through it (undefined uiState, dead picker buttons). ` +
4991
5001
  `Common causes: several controls inside a single <Field> (the Field provides one id via context and every control consumes it — put each control in its own <Field>, or give one an explicit id), ` +
4992
5002
  `or the same content mounted twice (e.g. two popups sharing an id prefix while both hold their content).`,
4993
5003
  );
@@ -5020,8 +5030,15 @@ const onUIStateControllerCreated = (uiStateController) => {
5020
5030
  const onUIStateControllerDestroyed = (uiStateController) => {
5021
5031
  const { id, name, controlType } = uiStateController;
5022
5032
  if (id) {
5023
- controllersById.delete(id);
5024
- naviJsRegistry.delete(id);
5033
+ // Only the controller the id currently points at may take the entry away:
5034
+ // when two controls share an id, the one leaving would otherwise unregister
5035
+ // the one staying (see the warning in onUIStateControllerCreated), and the
5036
+ // same holds while a control is being replaced by its successor, which
5037
+ // registers during its render, before this cleanup runs.
5038
+ if (controllersById.get(id) === uiStateController) {
5039
+ controllersById.delete(id);
5040
+ naviJsRegistry.delete(id);
5041
+ }
5025
5042
  }
5026
5043
  const proxyFor = uiStateController.props["navi-control-proxy-for"];
5027
5044
  if (proxyFor) {
@@ -35277,17 +35294,21 @@ const useUIStateController = (
35277
35294
  if (el) {
35278
35295
  el.__uiStateController__ = controller;
35279
35296
  }
35280
- // Re-register so the radio registry stays in sync when props.ref changes
35281
- // identity (e.g. across a Suspense boundary). The render-phase call in
35282
- // control_hooks.jsx handles the initial mount; this call handles re-runs.
35283
- onUIStateControllerCreated(controller);
35284
35297
  return () => {
35285
35298
  if (el && el.__uiStateController__ === controller) {
35286
35299
  delete el.__uiStateController__;
35287
35300
  }
35288
- onUIStateControllerDestroyed(controller);
35289
35301
  };
35290
35302
  }, [controllerRef]);
35303
+ // The registry entry lives as long as the component does. Registration is
35304
+ // done by the render (see useInteractiveProps); this cleanup is the only
35305
+ // thing that takes the entry away, so it must not follow the ref: a ref
35306
+ // identity change would run it right after the render that registered.
35307
+ useLayoutEffect(() => {
35308
+ return () => {
35309
+ onUIStateControllerDestroyed(controller);
35310
+ };
35311
+ }, []);
35291
35312
 
35292
35313
  const { parentUIStateController: parentController } = scope;
35293
35314
  useLayoutEffect(() => {
@@ -38916,11 +38937,13 @@ const useInteractiveProps = (props, {
38916
38937
  // an action's own completion side effect — has to read the signal to get an
38917
38938
  // answer that is not one frame late (see BUSY_CONSTRAINT).
38918
38939
  uiStateController.boundAction = boundAction;
38940
+ // "This id is that controller", stated on every render: a Suspense boundary
38941
+ // parking a subtree runs every hook cleanup below it — the unregistration
38942
+ // included — while the controllers themselves survive, and readers look a
38943
+ // controller up by id while rendering, before any effect can run again.
38944
+ // Placed after controlHostProps is set, which the constraints read.
38945
+ onUIStateControllerCreated(uiStateController);
38919
38946
  if (firstRender) {
38920
- // Deferred from the factory so these run after controlHostProps is set.
38921
- // Constraints like READONLY_CONSTRAINT and findControlProxyTargetController
38922
- // read controlHostProps — calling these earlier would throw or produce wrong results.
38923
- onUIStateControllerCreated(uiStateController);
38924
38947
  uiStateController.rules.validation.checkValidity();
38925
38948
  }
38926
38949
  return [controlRootProps, controlHostProps];
@@ -59548,6 +59571,15 @@ const css$E = /* css */`
59548
59571
  to filter). An entrance animation moves the dialog through scale and
59549
59572
  transform instead, which compose under it: see popup_css.js. */
59550
59573
 
59574
+ /* animation="growing" brings its own wall: the popup is the anchor
59575
+ continued, not a surface shown over a page that goes on being read (see
59576
+ navi_css_vars.js for the paint). Before the rules below, so everything
59577
+ said out loud — an outside press that captures, a backdropVariant, the
59578
+ two paint props — still wins over what the animation assumes. */
59579
+ &[data-growing] {
59580
+ --backdrop-background: var(--navi-backdrop-grow-background);
59581
+ --backdrop-filter: var(--navi-backdrop-grow-backdrop-filter);
59582
+ }
59551
59583
  &[data-pointer-interaction-outside="capture"] {
59552
59584
  --backdrop-background: var(--navi-backdrop-capture-background);
59553
59585
  --backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
@@ -59718,6 +59750,15 @@ const css$E = /* css */`
59718
59750
  --backdrop-background: var(--navi-backdrop-close-background);
59719
59751
  --backdrop-filter: var(--navi-backdrop-close-backdrop-filter);
59720
59752
  }
59753
+ /* animation="growing" brings its own wall: the popup is the anchor
59754
+ continued, not a surface shown over a page that goes on being read (see
59755
+ navi_css_vars.js for the paint). Before the rules below, so everything
59756
+ said out loud — an outside press that captures, a backdropVariant, the
59757
+ two paint props — still wins over what the animation assumes. */
59758
+ &[data-growing] {
59759
+ --backdrop-background: var(--navi-backdrop-grow-background);
59760
+ --backdrop-filter: var(--navi-backdrop-grow-backdrop-filter);
59761
+ }
59721
59762
  &[data-pointer-interaction-outside="capture"] {
59722
59763
  --backdrop-background: var(--navi-backdrop-capture-background);
59723
59764
  --backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
@@ -59935,7 +59976,8 @@ const css$E = /* css */`
59935
59976
  * @param {"auto"|"discrete"|"invisible"} [props.backdropVariant="auto"] - How
59936
59977
  * visible the backdrop is, independently of what it does. `"auto"`: the
59937
59978
  * paint `pointerInteractionOutsideEffect` implies (dimmed for
59938
- * `"close"`/`"cancel"`, blurred glass for `"capture"`). `"discrete"`: a
59979
+ * `"close"`/`"cancel"`, blurred glass for `"capture"`), or the opaque wall
59980
+ * `animation="growing"` asks for. `"discrete"`: a
59939
59981
  * barely-there dim. `"invisible"`: fully transparent — a wall that is not
59940
59982
  * seen, still catching every press. This only changes how much the dialog
59941
59983
  * insists visually, never what an outside click does; whether there is a
@@ -59968,7 +60010,11 @@ const css$E = /* css */`
59968
60010
  * over it. It needs an anchor (whatever opened the dialog, or the `anchor`
59969
60011
  * prop) and grows into whatever inside the dialog carries `data-grow`, the
59970
60012
  * dialog itself when nothing does. `"auto"` never picks it: only the caller
59971
- * knows the two boxes are one object. See `popup_grow.js`.
60013
+ * knows the two boxes are one object. It also brings its own backdrop —
60014
+ * opaque and blurred (`--navi-backdrop-grow-*`), the page it came out of
60015
+ * being what the movement leaves rather than a context to keep readable;
60016
+ * `backdropVariant="discrete"` asks for the light wash back. See
60017
+ * `popup_grow.js`.
59972
60018
  * @param {"box"|"scene"} [props.grow="box"] - Under `animation="growing"`,
59973
60019
  * what the anchor and what it grows into are to each other, which decides
59974
60020
  * how their pictures sit in the box moving between them. `"box"`: one
@@ -60998,6 +61044,7 @@ const useDialogProps = props => {
60998
61044
  "animationDuration": rest.animationDuration,
60999
61045
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
61000
61046
  "data-backdrop-variant": backdropVariant,
61047
+ "data-growing": growing ? "" : undefined,
61001
61048
  backdropColor,
61002
61049
  backdropFilter
61003
61050
  });
@@ -61033,6 +61080,11 @@ const useDialogProps = props => {
61033
61080
  // (and harmless for a local dialog, whose real backdrop element gets it
61034
61081
  // via backdropProps).
61035
61082
  "data-backdrop-variant": backdropVariant,
61083
+ // That this popup is the anchor continued rather than a surface shown
61084
+ // over the page, which is what its backdrop is painted from (see this
61085
+ // file's CSS). It cannot be read off navi-animation: growing arms no CSS
61086
+ // animation of its own, so that attribute is deliberately absent here.
61087
+ "data-growing": growing ? "" : undefined,
61036
61088
  // That this dialog has no wall, said in the DOM: the browser generates a
61037
61089
  // ::backdrop for the popover a wall-less top-layer dialog is shown as,
61038
61090
  // and the CSS above keys off this to leave it unpainted. Also the only
@@ -73306,7 +73358,9 @@ const PickerFirstResolver = props => {
73306
73358
  * (the popup itself when nothing does), and back on close. The trigger IS
73307
73359
  * the anchor, so nothing has to be named for it — `dialogSizeFromAnchor`
73308
73360
  * for a card that must keep its width on the way, left out for a drawing
73309
- * that opens precisely to get bigger.
73361
+ * that opens precisely to get bigger. It brings an opaque, blurred backdrop
73362
+ * with it (`--navi-backdrop-grow-*`); `backdropVariant="discrete"` asks for
73363
+ * the light wash back.
73310
73364
  * @param {"box"|"scene"} [grow="box"] Dialog's own, under `animation="growing"`:
73311
73365
  * `"box"` for a card that extends (its top stays, the box uncovers the rest),
73312
73366
  * `"scene"` for a thumbnail and the bigger view it is cut from (each picture