@jsenv/navi 0.29.186 → 0.29.187

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.
@@ -28906,7 +28906,7 @@ const FURNITURE_SELECTOR = `.navi_fixed_bar, .navi_popover:is(${TOP_LAYER_SELECT
28906
28906
  // owns it). Written out rather than imported: importing the module that owns
28907
28907
  // it back into this one would close a cycle.
28908
28908
  const TRANSITION_ATTRIBUTE$1 = "data-navi-route-transition";
28909
- const NAME_PROPERTY = "view-transition-name";
28909
+ const NAME_PROPERTY$1 = "view-transition-name";
28910
28910
  const FURNITURE_NAME_PREFIX = "navi-transition-furniture-";
28911
28911
 
28912
28912
  const nameByElement = new WeakMap();
@@ -28991,7 +28991,7 @@ const nameFurnitureAround = (areaElement) => {
28991
28991
  name = `${FURNITURE_NAME_PREFIX}${nameCount}`;
28992
28992
  nameByElement.set(element, name);
28993
28993
  }
28994
- element.style.setProperty(NAME_PROPERTY, name);
28994
+ element.style.setProperty(NAME_PROPERTY$1, name);
28995
28995
  namedElements.add(element);
28996
28996
  namesAdded.push(name);
28997
28997
  }
@@ -29027,7 +29027,7 @@ const releaseTransitionFurniture = (owner) => {
29027
29027
  }
29028
29028
  furnitureOwner = null;
29029
29029
  for (const element of namedElements) {
29030
- element.style.removeProperty(NAME_PROPERTY);
29030
+ element.style.removeProperty(NAME_PROPERTY$1);
29031
29031
  }
29032
29032
  namedElements = new Set();
29033
29033
  if (travelStyleElement) {
@@ -46134,6 +46134,43 @@ const createOpenController = (
46134
46134
  document.addEventListener("click", onCaptureClick, { capture: true });
46135
46135
  };
46136
46136
 
46137
+ // The DOM change a popup asked to have photographed (see
46138
+ // controller.transitionChange), waiting for the browser to take the picture
46139
+ // of the state being left. Anything the controller is asked to do meanwhile
46140
+ // happens after it: the change is run on the spot, and the transition holding
46141
+ // it finds nothing left to do.
46142
+ let changeAwaitingTransition = null;
46143
+ const flushChangeAwaitingTransition = () => {
46144
+ const change = changeAwaitingTransition;
46145
+ if (!change) {
46146
+ return;
46147
+ }
46148
+ changeAwaitingTransition = null;
46149
+ change();
46150
+ };
46151
+ const runChange = (change, { opened, event }) => {
46152
+ const { transitionChange } = controller;
46153
+ if (!transitionChange) {
46154
+ change();
46155
+ return;
46156
+ }
46157
+ // What the controller answers about itself does not wait for the picture:
46158
+ // whoever just asked reads `opened` on the spot (see
46159
+ // useOpenPropsEffectOnOpenController, which writes it back into the
46160
+ // caller's own signal), and a request arriving before the change lands
46161
+ // runs it first rather than reading a DOM that disagrees.
46162
+ controller.opened = opened;
46163
+ changeAwaitingTransition = change;
46164
+ transitionChange(
46165
+ () => {
46166
+ if (changeAwaitingTransition === change) {
46167
+ flushChangeAwaitingTransition();
46168
+ }
46169
+ },
46170
+ { opened, event },
46171
+ );
46172
+ };
46173
+
46137
46174
  const performClose = (closeEvent) => {
46138
46175
  controller.opened = false;
46139
46176
  // Read before any close effect touches the DOM: closing a native <dialog>
@@ -46216,18 +46253,23 @@ const createOpenController = (
46216
46253
  }
46217
46254
  }
46218
46255
 
46219
- // Sync the DOM closed first (releasing the focus trap) — only then run
46220
- // the owner's own reaction (onClose may restore focus to an element
46221
- // outside the popup, which the focus trap would otherwise fight while
46222
- // still active).
46223
- openEffectCleanup?.(closeEvent);
46224
- openEffectCleanup = null;
46225
- closeHandlers?.onClose?.(closeEvent);
46226
- closeHandlers = null;
46227
- // Last: the close effects above are what starts the exit transition the
46228
- // content must outlive (see popup_content_mount.js).
46229
- controller.unmountContent?.();
46230
- controller.onOpenedChange?.(false, closeEvent);
46256
+ runChange(
46257
+ () => {
46258
+ // Sync the DOM closed first (releasing the focus trap) — only then run
46259
+ // the owner's own reaction (onClose may restore focus to an element
46260
+ // outside the popup, which the focus trap would otherwise fight while
46261
+ // still active).
46262
+ openEffectCleanup?.(closeEvent);
46263
+ openEffectCleanup = null;
46264
+ closeHandlers?.onClose?.(closeEvent);
46265
+ closeHandlers = null;
46266
+ // Last: the close effects above are what starts the exit transition the
46267
+ // content must outlive (see popup_content_mount.js).
46268
+ controller.unmountContent?.();
46269
+ controller.onOpenedChange?.(false, closeEvent);
46270
+ },
46271
+ { opened: false, event: closeEvent },
46272
+ );
46231
46273
  };
46232
46274
  const controller = {
46233
46275
  opened: false,
@@ -46246,6 +46288,15 @@ const createOpenController = (
46246
46288
  // The counterpart, set only when the popup was told to throw its content
46247
46289
  // away on close (`mount="while-opened"`). Called from performClose above.
46248
46290
  unmountContent: null,
46291
+ // Set by the controlled element when the DOM change that opens or closes
46292
+ // it has to be photographed by the browser on both sides — a document view
46293
+ // transition, whose update callback is the only place that change can
46294
+ // happen (Dialog's animation="growing", see popup_grow.js). Called with
46295
+ // the change and where it leads; running it is its job, and it may run it
46296
+ // a frame later than it was asked for. That delay is the reason a popup
46297
+ // cannot do this from the outside: `--navi-open` runs when navi runs it,
46298
+ // and a close arrives once the DOM already holds it.
46299
+ transitionChange: null,
46249
46300
  // Told whenever `opened` actually changes, whatever asked for it — an
46250
46301
  // interaction, a command, a prop — with the event that asked. What lets a
46251
46302
  // `signal` prop reflect the popup's real state, and a `navState` prop write
@@ -46253,6 +46304,7 @@ const createOpenController = (
46253
46304
  // called once the open/close has fully happened rather than mid-sequence.
46254
46305
  onOpenedChange: null,
46255
46306
  open: (e, detail) => {
46307
+ flushChangeAwaitingTransition();
46256
46308
  if (controller.opened || !controller.openEffect) {
46257
46309
  return;
46258
46310
  }
@@ -46328,38 +46380,44 @@ const createOpenController = (
46328
46380
  }
46329
46381
  };
46330
46382
  };
46331
- // Before mountContent, which builds the content, and before openEffect,
46332
- // which shows it: what the popup opens ON has to be known before either
46333
- // (see `onOpen` above).
46334
- controller.onOpen?.(requestOpenEvent);
46335
- // After prepareFocusTransfer, which has to record what held the focus
46336
- // before anything inside the popup can claim it, and before openEffect,
46337
- // which measures the popup to place it.
46338
- controller.mountContent?.();
46339
- // Only now — after the content has been built, before openEffect shows
46340
- // it. Dialog/Popover recompute aria-expanded and navi-hidden from this
46341
- // flag on every render, and mountContent above renders synchronously:
46342
- // flipping it any earlier commits an already-open DOM (aria-expanded
46343
- // "true", navi-hidden gone) before openEffect has run a single
46344
- // statement, so the "closed" frame it pins to transition from is in
46345
- // fact the open one and the entrance animation has nothing to play.
46346
- // It also gives the content it just built the opening it is documented
46347
- // to observe — mounted while the popup reads as closed, told it opened
46348
- // right after (see popup_content_mount.js and
46349
- // use_displayed_layout_effect.js).
46350
- controller.opened = true;
46351
- const openEffectReturnValue =
46352
- controller.openEffect(requestOpenEvent) || null;
46353
- openEffectCleanup = (closeEvent) => {
46354
- openEffectReturnValue?.(closeEvent);
46355
- };
46356
- closeHandlers = openHandler(requestOpenEvent) || null;
46357
- controller.onOpenedChange?.(true, requestOpenEvent);
46383
+ runChange(
46384
+ () => {
46385
+ // Before mountContent, which builds the content, and before
46386
+ // openEffect, which shows it: what the popup opens ON has to be
46387
+ // known before either (see `onOpen` above).
46388
+ controller.onOpen?.(requestOpenEvent);
46389
+ // After prepareFocusTransfer, which has to record what held the
46390
+ // focus before anything inside the popup can claim it, and before
46391
+ // openEffect, which measures the popup to place it.
46392
+ controller.mountContent?.();
46393
+ // Only now — after the content has been built, before openEffect
46394
+ // shows it. Dialog/Popover recompute aria-expanded and navi-hidden
46395
+ // from this flag on every render, and mountContent above renders
46396
+ // synchronously: flipping it any earlier commits an already-open DOM
46397
+ // (aria-expanded "true", navi-hidden gone) before openEffect has run
46398
+ // a single statement, so the "closed" frame it pins to transition
46399
+ // from is in fact the open one and the entrance animation has
46400
+ // nothing to play. It also gives the content it just built the
46401
+ // opening it is documented to observe — mounted while the popup
46402
+ // reads as closed, told it opened right after (see
46403
+ // popup_content_mount.js and use_displayed_layout_effect.js).
46404
+ controller.opened = true;
46405
+ const openEffectReturnValue =
46406
+ controller.openEffect(requestOpenEvent) || null;
46407
+ openEffectCleanup = (closeEvent) => {
46408
+ openEffectReturnValue?.(closeEvent);
46409
+ };
46410
+ closeHandlers = openHandler(requestOpenEvent) || null;
46411
+ controller.onOpenedChange?.(true, requestOpenEvent);
46412
+ },
46413
+ { opened: true, event: requestOpenEvent },
46414
+ );
46358
46415
  },
46359
46416
  requestClose: (
46360
46417
  e = new CustomEvent("programmatic", { detail: {} }),
46361
46418
  detail,
46362
46419
  ) => {
46420
+ flushChangeAwaitingTransition();
46363
46421
  if (!controller.opened) {
46364
46422
  return true;
46365
46423
  }
@@ -46382,6 +46440,7 @@ const createOpenController = (
46382
46440
  return true;
46383
46441
  },
46384
46442
  close: (e = new CustomEvent("programmatic", { detail: {} }), detail) => {
46443
+ flushChangeAwaitingTransition();
46385
46444
  if (!controller.opened) {
46386
46445
  return;
46387
46446
  }
@@ -46432,6 +46491,11 @@ const useOpenController = (openHandler) => {
46432
46491
  // no choice to leave open — close it for real.
46433
46492
  useLayoutEffect(() => {
46434
46493
  return () => {
46494
+ // Nothing to photograph on the way out: the popup is leaving the
46495
+ // document, so a movement between its box and anything else would be
46496
+ // played on an element already detached by the time the browser gets to
46497
+ // it (see controller.transitionChange).
46498
+ controllerRef.current.transitionChange = null;
46435
46499
  controllerRef.current.close();
46436
46500
  };
46437
46501
  }, []);
@@ -58463,6 +58527,146 @@ const translateOf = (axis, distance) =>
58463
58527
  ? `translate(${distance}px, 0px)`
58464
58528
  : `translate(0px, ${distance}px)`;
58465
58529
 
58530
+ /**
58531
+ * A popup that is the anchor, continued.
58532
+ *
58533
+ * Opening a surface out of the element that asked for it is not the surface
58534
+ * appearing while that element stays put: it is one box becoming another. The
58535
+ * browser draws exactly that on its own — the same `view-transition-name` on
58536
+ * the anchor before the change and on the popup after it, and it morphs the
58537
+ * first box into the second — so all this file does is hand that name over at
58538
+ * the two instants it can be handed over, and give it back afterwards.
58539
+ *
58540
+ * The pictures are taken around a DOM change, which is why the movement cannot
58541
+ * be written from outside: what a caller can reach — a command, `onClose` — is
58542
+ * either before the change or after it, never around it. The change that opens
58543
+ * a popup, and above all the one that closes it, belongs to the open
58544
+ * controller, which is what `transitionChange` (open_controller.js) exists to
58545
+ * let a popup wrap.
58546
+ *
58547
+ * One name serves the whole movement, because only one of the two boxes is on
58548
+ * screen at a time: it names the anchor while the popup is closed, and the
58549
+ * popup while it is open. What continues the anchor INSIDE the popup is
58550
+ * whatever carries `data-grow`, and the popup itself when nothing does — a
58551
+ * dialog holding the grown card plus buttons around it grows the card, a
58552
+ * dialog that IS the grown thing grows whole.
58553
+ *
58554
+ * The page around stays out of the picture (`view-transition-name: none` on
58555
+ * the root, in dialog.jsx): a captured element is not painted where it stands
58556
+ * and cannot be pointed at either, so photographing the whole document would
58557
+ * leave the page frozen and unpressable for the length of every opening — and
58558
+ * of every closing, which is the moment the user is coming back to it.
58559
+ */
58560
+
58561
+
58562
+ // The name the two boxes take turns wearing. A single literal one is enough,
58563
+ // and unique by construction: a document has one view transition, so it has at
58564
+ // most one popup growing, and the movement being replaced gives the name back
58565
+ // before the next one takes it (see releaseGrowInProgress).
58566
+ const NAME = "navi-popup-grow";
58567
+ const NAME_PROPERTY = "view-transition-name";
58568
+ // Worn by the root for the length of the movement — what the CSS keys the
58569
+ // page's own opt-out on, and the movement's only trace in the document.
58570
+ const ROOT_ATTRIBUTE = "data-navi-popup-grow";
58571
+ // Inside the popup, the one node that IS the anchor once it has grown.
58572
+ const TARGET_SELECTOR = "[data-grow]";
58573
+ // The popup's own animation duration, published on the root because the
58574
+ // ::view-transition tree hangs off it and inherits from nowhere else.
58575
+ const DURATION_PROPERTY = "--navi-popup-grow-duration";
58576
+
58577
+ let releaseGrowInProgress = null;
58578
+
58579
+ /**
58580
+ * Runs `applyChange` — the DOM change that opens or closes `popupEl` — inside
58581
+ * a view transition morphing the anchor's box into the popup's, or back.
58582
+ *
58583
+ * `opened` says which way: the box being left is the anchor when the popup is
58584
+ * opening and the popup when it is closing, and the arriving one is only known
58585
+ * once the change has been made (the content a popup grows into is built by
58586
+ * that very change).
58587
+ */
58588
+ const growPopupFromAnchor = (
58589
+ popupEl,
58590
+ anchorElement,
58591
+ applyChange,
58592
+ { opened },
58593
+ ) => {
58594
+ const startViewTransition = ensureDocumentStartViewTransition();
58595
+ // A movement still wearing the name would make the name two elements wide,
58596
+ // and a name belonging to two elements aborts the transition for the whole
58597
+ // document.
58598
+ releaseGrowInProgress?.();
58599
+
58600
+ const elementLeaving = opened ? anchorElement : resolveGrowTarget(popupEl);
58601
+ const giveBackNameLeaving = wearGrowName(elementLeaving);
58602
+ const root = document.documentElement;
58603
+ root.setAttribute(ROOT_ATTRIBUTE, "");
58604
+ const duration = getComputedStyle(popupEl)
58605
+ .getPropertyValue("--popup-animation-duration")
58606
+ .trim();
58607
+ if (duration) {
58608
+ // Empty would substitute into `animation-duration:` as nothing at all,
58609
+ // which computes to 0s — a movement nobody sees rather than one at the
58610
+ // browser's own pace.
58611
+ root.style.setProperty(DURATION_PROPERTY, duration);
58612
+ }
58613
+
58614
+ let giveBackNameArriving = null;
58615
+ const release = () => {
58616
+ if (releaseGrowInProgress !== release) {
58617
+ return;
58618
+ }
58619
+ releaseGrowInProgress = null;
58620
+ giveBackNameLeaving();
58621
+ giveBackNameArriving?.();
58622
+ root.removeAttribute(ROOT_ATTRIBUTE);
58623
+ root.style.removeProperty(DURATION_PROPERTY);
58624
+ };
58625
+ releaseGrowInProgress = release;
58626
+
58627
+ const viewTransition = startViewTransition(() => {
58628
+ // The name is the arriving box's from here on: worn by both, it is worn by
58629
+ // neither. Written rather than removed, so a name the element also has
58630
+ // from a stylesheet cannot resurface for the length of the movement.
58631
+ elementLeaving.style.setProperty(NAME_PROPERTY, "none");
58632
+ applyChange();
58633
+ const elementArriving = opened
58634
+ ? resolveGrowTarget(popupEl)
58635
+ : // Gone from the document while the popup was open (the row it stood
58636
+ // in was removed): nothing to arrive at, and the browser plays the
58637
+ // popup's picture out on its own.
58638
+ anchorElement.isConnected
58639
+ ? anchorElement
58640
+ : null;
58641
+ if (elementArriving) {
58642
+ giveBackNameArriving = wearGrowName(elementArriving);
58643
+ }
58644
+ });
58645
+ viewTransition.finished.then(release, release);
58646
+ };
58647
+
58648
+ const resolveGrowTarget = (popupEl) => {
58649
+ const declaredTarget = popupEl.querySelector(TARGET_SELECTOR);
58650
+ if (declaredTarget) {
58651
+ return declaredTarget;
58652
+ }
58653
+ return popupEl;
58654
+ };
58655
+
58656
+ // Wears the movement's name, and gives back whatever the element had written
58657
+ // inline of its own once the movement is over.
58658
+ const wearGrowName = (element) => {
58659
+ const nameBefore = element.style.getPropertyValue(NAME_PROPERTY);
58660
+ element.style.setProperty(NAME_PROPERTY, NAME);
58661
+ return () => {
58662
+ if (nameBefore) {
58663
+ element.style.setProperty(NAME_PROPERTY, nameBefore);
58664
+ return;
58665
+ }
58666
+ element.style.removeProperty(NAME_PROPERTY);
58667
+ };
58668
+ };
58669
+
58466
58670
  const PopupClose = ({
58467
58671
  label,
58468
58672
  iconSize,
@@ -58512,12 +58716,14 @@ installImportMetaCssBuild(import.meta);/**
58512
58716
  * popup_shared.js), even though several combinations land identically here
58513
58717
  * since Dialog is never really anchored — kept distinct anyway because
58514
58718
  * `positionArea` still picks which animation direction plays. `anchor` is
58515
- * inert here unless `sizeFromAnchor` asks for it: a dialog is a surface of
58516
- * its own, sized by its content, not a panel grown out of the control that
58517
- * opened it — that is Popover's job. With `sizeFromAnchor`, the anchor's box
58518
- * reaches the `--anchor-width`/`--anchor-height` CSS vars and becomes a
58519
- * min-width/min-height floor. Either way Dialog's own positioning is never
58520
- * relative to the anchor, unlike Popover.
58719
+ * inert here unless something asks for it: a dialog is a surface of its own,
58720
+ * sized by its content, not a panel grown out of the control that opened it —
58721
+ * that is Popover's job. Two props ask. With `sizeFromAnchor`, the anchor's
58722
+ * box reaches the `--anchor-width`/`--anchor-height` CSS vars and becomes a
58723
+ * min-width/min-height floor. With `animation="growing"`, the anchor's box is
58724
+ * where the dialog comes from: the two are photographed and morphed into each
58725
+ * other, and back on close (popup_grow.js). Either way Dialog's own
58726
+ * positioning is never relative to the anchor, unlike Popover.
58521
58727
  *
58522
58728
  * Two rendering strategies, picked via `layer`: `DialogAsModal` (a real
58523
58729
  * `<dialog>`, `showModal()`, top layer — native focus trap,
@@ -58962,6 +59168,27 @@ const css$E = /* css */`
58962
59168
  }
58963
59169
  }
58964
59170
 
59171
+ /* While a dialog is growing out of the element that opened it
59172
+ (popup_grow.js). The page around is deliberately NOT taken as a picture,
59173
+ against the browser's own default: a captured element is not painted where
59174
+ it stands and cannot be pointed at either, so photographing the document
59175
+ would leave the whole page frozen and unpressable for the length of every
59176
+ opening — and of every closing, which is the moment the user is coming
59177
+ back to it. Nothing shows through where the movement is: the two boxes it
59178
+ plays between are captured, and their pictures cover the rectangle between
59179
+ them at every moment. */
59180
+ :root[data-navi-popup-grow] {
59181
+ view-transition-name: none;
59182
+
59183
+ /* The popup's own pace, published on the root by popup_grow.js because the
59184
+ ::view-transition tree hangs off it and inherits from nowhere else. */
59185
+ &::view-transition-group(navi-popup-grow),
59186
+ &::view-transition-old(navi-popup-grow),
59187
+ &::view-transition-new(navi-popup-grow) {
59188
+ animation-duration: var(--navi-popup-grow-duration, 0.25s);
59189
+ }
59190
+ }
59191
+
58965
59192
  ${surfaceTextCss}
58966
59193
  ${popupCss}
58967
59194
  `;
@@ -59055,16 +59282,25 @@ const css$E = /* css */`
59055
59282
  * scroll while open (its backdrop only covers the scrollport, so scrolling
59056
59283
  * there would reveal uncovered content); this prop extends the lock to the
59057
59284
  * whole page. Defaults to `true` for a dialog docked by `dockedOnSmallTouchScreen`.
59058
- * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`} [props.animation]
59285
+ * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|"growing"|`slide-from-${string}`} [props.animation]
59059
59286
  * - `true`/`"auto"` resolves to `"scaling"` for a centered `positionArea`,
59060
59287
  * or a concrete `"slide-from-*"` direction otherwise. Any other explicit
59061
- * value is used as-is.
59288
+ * value is used as-is. `"growing"` is the odd one out: every other kind
59289
+ * moves the dialog on its own box (it scales from its own centre, slides in
59290
+ * from its own edge), while this one is the browser morphing the ANCHOR's
59291
+ * box into the dialog's, and the dialog's back into the anchor's on close —
59292
+ * for a dialog that is what the anchor became rather than a surface shown
59293
+ * over it. It needs an anchor (whatever opened the dialog, or the `anchor`
59294
+ * prop) and grows into whatever inside the dialog carries `data-grow`, the
59295
+ * dialog itself when nothing does. `"auto"` never picks it: only the caller
59296
+ * knows the two boxes are one object. See `popup_grow.js`.
59062
59297
  * @param {string} [props.animationDuration] - Maps to
59063
59298
  * `--popup-animation-duration`.
59064
59299
  * @param {Element|{current: Element}|string} [props.anchor] - Never used for
59065
59300
  * positioning (see this file's top comment), and ignored entirely unless
59066
- * `sizeFromAnchor` is set — then it sizes the dialog via the
59067
- * `--anchor-width`/`--anchor-height` CSS vars. Defaults to whatever
59301
+ * `sizeFromAnchor` or `animation="growing"` asks for it — the first sizes
59302
+ * the dialog via the `--anchor-width`/`--anchor-height` CSS vars, the second
59303
+ * makes the dialog come out of the anchor's own box. Defaults to whatever
59068
59304
  * triggered the open (`e.detail.anchor`), if any. A string is resolved via
59069
59305
  * `document.getElementById` when the dialog opens — see popover.jsx's own
59070
59306
  * `anchor` doc for why (mainly `defaultOpen`).
@@ -59461,6 +59697,8 @@ const useDialogProps = props => {
59461
59697
  marginWithContainer
59462
59698
  };
59463
59699
  const repositionRef = useRef(null);
59700
+ // The element this dialog came out of, held from the opening to the closing.
59701
+ const anchorElementRef = useRef(null);
59464
59702
  useEffect(() => {
59465
59703
  repositionRef.current?.(new CustomEvent("position_props_change", {
59466
59704
  detail: {}
@@ -59519,20 +59757,87 @@ const useDialogProps = props => {
59519
59757
  flushEdges.right = expandX || x === "right" || x === "inset-right";
59520
59758
  }
59521
59759
  const isAutoAnimation = animation === true || animation === "auto";
59522
- // Dialog never has a real anchor (see this file's top comment), so this
59523
- // is always the "no anchor" path — the same one Popover's own custom
59524
- // renderer falls into when it has no real anchor either.
59760
+ // The dialog and the anchor are one box, and what plays between them is the
59761
+ // browser's own morph (popup_grow.js) — nothing this dialog does to its own
59762
+ // box. So it arms no CSS transition of its own, which is not merely useless
59763
+ // here but wrong: the shared [navi-animation] rule transitions `display`
59764
+ // with allow-discrete, and a dialog kept rendered for the length of its exit
59765
+ // is exactly what the picture taken of the state it closes into must not
59766
+ // show.
59767
+ const growing = animation === "growing";
59768
+ // Dialog never has a real anchor to POSITION against (see this file's top
59769
+ // comment), so this is always the "no anchor" path — the same one Popover's
59770
+ // own custom renderer falls into when it has no real anchor either.
59525
59771
  const resolvedAnimationKind = isAutoAnimation ? resolveAutoAnimationKind(undefined, parsedPositionArea) : animation;
59526
59772
  // Not gated on isAutoAnimation — an explicit animation="sliding" needs a
59527
59773
  // concrete direction just as much as an auto-resolved one does (same as
59528
59774
  // Popover's own "sliding"/"expanding" resolution step in openEffect).
59529
- let resolvedAnimation = resolvedAnimationKind;
59775
+ let resolvedAnimation = growing ? undefined : resolvedAnimationKind;
59530
59776
  if (resolvedAnimationKind === "sliding") {
59531
59777
  resolvedAnimation = resolveDirectionValue(parsedPositionArea.y, parsedPositionArea.x, {
59532
59778
  prefix: "slide-from"
59533
59779
  }) ?? "slide-from-top";
59534
59780
  }
59535
59781
 
59782
+ // What this dialog was opened out of, resolved at open time rather than at
59783
+ // render time — mainly for defaultOpen, where there's no triggering
59784
+ // event/ref yet to read a real element from, only an id known up front. See
59785
+ // popover.jsx's own anchor handling for the full reasoning, mirrored here
59786
+ // identically.
59787
+ const resolveAnchorElement = e => {
59788
+ if (typeof anchor === "string") {
59789
+ const anchorElementById = document.getElementById(anchor);
59790
+ if (!anchorElementById) {
59791
+ console.warn(`Dialog: anchor="${anchor}" did not match any element`);
59792
+ }
59793
+ return anchorElementById;
59794
+ }
59795
+ if (anchor) {
59796
+ // anchor prop is a ref or a DOM element — always a real anchor,
59797
+ // regardless of anchorCustomEventDetail. A ref is unwrapped even when it
59798
+ // holds nothing: falling back to the ref object itself would pass the
59799
+ // "is there an anchor?" tests downstream with something that has no box.
59800
+ return "current" in anchor ? anchor.current : anchor;
59801
+ }
59802
+ if (anchorCustomEventDetail === "override") {
59803
+ // e.g. the button that triggered a --navi-toggle/--navi-open command,
59804
+ // already resolved from detail.anchor/detail.source by the caller
59805
+ // (see UncontrolledDialog's onnavi_request_open).
59806
+ return e.detail.anchor;
59807
+ }
59808
+ return undefined;
59809
+ };
59810
+
59811
+ // The dialog and the anchor are the same box at two sizes, so the opening
59812
+ // and the closing are one becoming the other. The browser draws that itself
59813
+ // provided the change happens between its two pictures, which is what
59814
+ // handing it to the controller buys (see popup_grow.js and
59815
+ // open_controller.js's own transitionChange).
59816
+ openController.transitionChange = growing ? (applyChange, {
59817
+ opened,
59818
+ event
59819
+ }) => {
59820
+ const dialogEl = ref.current;
59821
+ // A mount-time opening was never seen closed (see openEffect's own
59822
+ // `silent`): there is no box it comes from, because nothing was shown
59823
+ // before it.
59824
+ if (!dialogEl || opened && event.detail.silent) {
59825
+ applyChange();
59826
+ return;
59827
+ }
59828
+ const anchorElement = opened ? resolveAnchorElement(event) : anchorElementRef.current;
59829
+ if (!anchorElement) {
59830
+ if (opened) {
59831
+ console.warn(`[navi] Dialog has animation="growing" and no anchor to grow out of, so it simply appears. The anchor is whatever opened it — a <Button command="--navi-open">, the "source" given to triggerNaviCommand — or the "anchor" prop.`);
59832
+ }
59833
+ applyChange();
59834
+ return;
59835
+ }
59836
+ growPopupFromAnchor(dialogEl, anchorElement, applyChange, {
59837
+ opened
59838
+ });
59839
+ } : null;
59840
+
59536
59841
  // Sync the DOM open and return how to sync it back closed, fresh on every
59537
59842
  // render so it closes over the latest props (scrollLock, etc.). The
59538
59843
  // controller (owned by the caller, or by UncontrolledDialog) decides
@@ -59562,28 +59867,10 @@ const useDialogProps = props => {
59562
59867
  // real, meaningful ancestor beyond it.
59563
59868
  const positionedAncestor = isModal ? document.documentElement : getPositionedParent(dialogEl.parentElement /* dialogEl is inside the clip_wrapper */);
59564
59869
  const [cleanup, addCleanup] = createPubSub(true);
59565
- let anchorElement;
59566
- if (typeof anchor === "string") {
59567
- // Resolved at open time (not render time) via getElementById — mainly
59568
- // for defaultOpen, where there's no triggering event/ref yet to read a
59569
- // real element from, only an id known up front. See popover.jsx's own
59570
- // anchor handling for the full reasoning, mirrored here identically.
59571
- anchorElement = document.getElementById(anchor);
59572
- if (!anchorElement) {
59573
- console.warn(`Dialog: anchor="${anchor}" did not match any element`);
59574
- }
59575
- } else if (anchor) {
59576
- // anchor prop is a ref or a DOM element — always a real anchor,
59577
- // regardless of anchorCustomEventDetail. A ref is unwrapped even when it
59578
- // holds nothing: falling back to the ref object itself would pass the
59579
- // "is there an anchor?" test below with something that has no box.
59580
- anchorElement = "current" in anchor ? anchor.current : anchor;
59581
- } else if (anchorCustomEventDetail === "override") {
59582
- // e.g. the button that triggered a --navi-toggle/--navi-open command,
59583
- // already resolved from detail.anchor/detail.source by the caller
59584
- // (see UncontrolledDialog's onnavi_request_open).
59585
- anchorElement = e.detail.anchor;
59586
- }
59870
+ const anchorElement = resolveAnchorElement(e);
59871
+ // Kept for the closing, which has no opening event to read it back from:
59872
+ // the box the dialog shrinks back into is the one it came out of.
59873
+ anchorElementRef.current = anchorElement;
59587
59874
  debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> openDialog`);
59588
59875
  if (!isModal) {
59589
59876
  // see openLocalDialogCount's own comment