@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.
@@ -27373,7 +27373,7 @@ const FURNITURE_SELECTOR = `.navi_fixed_bar, .navi_popover:is(${TOP_LAYER_SELECT
27373
27373
  // owns it). Written out rather than imported: importing the module that owns
27374
27374
  // it back into this one would close a cycle.
27375
27375
  const TRANSITION_ATTRIBUTE$1 = "data-navi-route-transition";
27376
- const NAME_PROPERTY = "view-transition-name";
27376
+ const NAME_PROPERTY$1 = "view-transition-name";
27377
27377
  const FURNITURE_NAME_PREFIX = "navi-transition-furniture-";
27378
27378
 
27379
27379
  const nameByElement = new WeakMap();
@@ -27458,7 +27458,7 @@ const nameFurnitureAround = (areaElement) => {
27458
27458
  name = `${FURNITURE_NAME_PREFIX}${nameCount}`;
27459
27459
  nameByElement.set(element, name);
27460
27460
  }
27461
- element.style.setProperty(NAME_PROPERTY, name);
27461
+ element.style.setProperty(NAME_PROPERTY$1, name);
27462
27462
  namedElements.add(element);
27463
27463
  namesAdded.push(name);
27464
27464
  }
@@ -27494,7 +27494,7 @@ const releaseTransitionFurniture = (owner) => {
27494
27494
  }
27495
27495
  furnitureOwner = null;
27496
27496
  for (const element of namedElements) {
27497
- element.style.removeProperty(NAME_PROPERTY);
27497
+ element.style.removeProperty(NAME_PROPERTY$1);
27498
27498
  }
27499
27499
  namedElements = new Set();
27500
27500
  if (travelStyleElement) {
@@ -44470,6 +44470,43 @@ const createOpenController = (
44470
44470
  document.addEventListener("click", onCaptureClick, { capture: true });
44471
44471
  };
44472
44472
 
44473
+ // The DOM change a popup asked to have photographed (see
44474
+ // controller.transitionChange), waiting for the browser to take the picture
44475
+ // of the state being left. Anything the controller is asked to do meanwhile
44476
+ // happens after it: the change is run on the spot, and the transition holding
44477
+ // it finds nothing left to do.
44478
+ let changeAwaitingTransition = null;
44479
+ const flushChangeAwaitingTransition = () => {
44480
+ const change = changeAwaitingTransition;
44481
+ if (!change) {
44482
+ return;
44483
+ }
44484
+ changeAwaitingTransition = null;
44485
+ change();
44486
+ };
44487
+ const runChange = (change, { opened, event }) => {
44488
+ const { transitionChange } = controller;
44489
+ if (!transitionChange) {
44490
+ change();
44491
+ return;
44492
+ }
44493
+ // What the controller answers about itself does not wait for the picture:
44494
+ // whoever just asked reads `opened` on the spot (see
44495
+ // useOpenPropsEffectOnOpenController, which writes it back into the
44496
+ // caller's own signal), and a request arriving before the change lands
44497
+ // runs it first rather than reading a DOM that disagrees.
44498
+ controller.opened = opened;
44499
+ changeAwaitingTransition = change;
44500
+ transitionChange(
44501
+ () => {
44502
+ if (changeAwaitingTransition === change) {
44503
+ flushChangeAwaitingTransition();
44504
+ }
44505
+ },
44506
+ { opened, event },
44507
+ );
44508
+ };
44509
+
44473
44510
  const performClose = (closeEvent) => {
44474
44511
  controller.opened = false;
44475
44512
  // Read before any close effect touches the DOM: closing a native <dialog>
@@ -44552,18 +44589,23 @@ const createOpenController = (
44552
44589
  }
44553
44590
  }
44554
44591
 
44555
- // Sync the DOM closed first (releasing the focus trap) — only then run
44556
- // the owner's own reaction (onClose may restore focus to an element
44557
- // outside the popup, which the focus trap would otherwise fight while
44558
- // still active).
44559
- openEffectCleanup?.(closeEvent);
44560
- openEffectCleanup = null;
44561
- closeHandlers?.onClose?.(closeEvent);
44562
- closeHandlers = null;
44563
- // Last: the close effects above are what starts the exit transition the
44564
- // content must outlive (see popup_content_mount.js).
44565
- controller.unmountContent?.();
44566
- controller.onOpenedChange?.(false, closeEvent);
44592
+ runChange(
44593
+ () => {
44594
+ // Sync the DOM closed first (releasing the focus trap) — only then run
44595
+ // the owner's own reaction (onClose may restore focus to an element
44596
+ // outside the popup, which the focus trap would otherwise fight while
44597
+ // still active).
44598
+ openEffectCleanup?.(closeEvent);
44599
+ openEffectCleanup = null;
44600
+ closeHandlers?.onClose?.(closeEvent);
44601
+ closeHandlers = null;
44602
+ // Last: the close effects above are what starts the exit transition the
44603
+ // content must outlive (see popup_content_mount.js).
44604
+ controller.unmountContent?.();
44605
+ controller.onOpenedChange?.(false, closeEvent);
44606
+ },
44607
+ { opened: false, event: closeEvent },
44608
+ );
44567
44609
  };
44568
44610
  const controller = {
44569
44611
  opened: false,
@@ -44582,6 +44624,15 @@ const createOpenController = (
44582
44624
  // The counterpart, set only when the popup was told to throw its content
44583
44625
  // away on close (`mount="while-opened"`). Called from performClose above.
44584
44626
  unmountContent: null,
44627
+ // Set by the controlled element when the DOM change that opens or closes
44628
+ // it has to be photographed by the browser on both sides — a document view
44629
+ // transition, whose update callback is the only place that change can
44630
+ // happen (Dialog's animation="growing", see popup_grow.js). Called with
44631
+ // the change and where it leads; running it is its job, and it may run it
44632
+ // a frame later than it was asked for. That delay is the reason a popup
44633
+ // cannot do this from the outside: `--navi-open` runs when navi runs it,
44634
+ // and a close arrives once the DOM already holds it.
44635
+ transitionChange: null,
44585
44636
  // Told whenever `opened` actually changes, whatever asked for it — an
44586
44637
  // interaction, a command, a prop — with the event that asked. What lets a
44587
44638
  // `signal` prop reflect the popup's real state, and a `navState` prop write
@@ -44589,6 +44640,7 @@ const createOpenController = (
44589
44640
  // called once the open/close has fully happened rather than mid-sequence.
44590
44641
  onOpenedChange: null,
44591
44642
  open: (e, detail) => {
44643
+ flushChangeAwaitingTransition();
44592
44644
  if (controller.opened || !controller.openEffect) {
44593
44645
  return;
44594
44646
  }
@@ -44664,38 +44716,44 @@ const createOpenController = (
44664
44716
  }
44665
44717
  };
44666
44718
  };
44667
- // Before mountContent, which builds the content, and before openEffect,
44668
- // which shows it: what the popup opens ON has to be known before either
44669
- // (see `onOpen` above).
44670
- controller.onOpen?.(requestOpenEvent);
44671
- // After prepareFocusTransfer, which has to record what held the focus
44672
- // before anything inside the popup can claim it, and before openEffect,
44673
- // which measures the popup to place it.
44674
- controller.mountContent?.();
44675
- // Only now — after the content has been built, before openEffect shows
44676
- // it. Dialog/Popover recompute aria-expanded and navi-hidden from this
44677
- // flag on every render, and mountContent above renders synchronously:
44678
- // flipping it any earlier commits an already-open DOM (aria-expanded
44679
- // "true", navi-hidden gone) before openEffect has run a single
44680
- // statement, so the "closed" frame it pins to transition from is in
44681
- // fact the open one and the entrance animation has nothing to play.
44682
- // It also gives the content it just built the opening it is documented
44683
- // to observe — mounted while the popup reads as closed, told it opened
44684
- // right after (see popup_content_mount.js and
44685
- // use_displayed_layout_effect.js).
44686
- controller.opened = true;
44687
- const openEffectReturnValue =
44688
- controller.openEffect(requestOpenEvent) || null;
44689
- openEffectCleanup = (closeEvent) => {
44690
- openEffectReturnValue?.(closeEvent);
44691
- };
44692
- closeHandlers = openHandler(requestOpenEvent) || null;
44693
- controller.onOpenedChange?.(true, requestOpenEvent);
44719
+ runChange(
44720
+ () => {
44721
+ // Before mountContent, which builds the content, and before
44722
+ // openEffect, which shows it: what the popup opens ON has to be
44723
+ // known before either (see `onOpen` above).
44724
+ controller.onOpen?.(requestOpenEvent);
44725
+ // After prepareFocusTransfer, which has to record what held the
44726
+ // focus before anything inside the popup can claim it, and before
44727
+ // openEffect, which measures the popup to place it.
44728
+ controller.mountContent?.();
44729
+ // Only now — after the content has been built, before openEffect
44730
+ // shows it. Dialog/Popover recompute aria-expanded and navi-hidden
44731
+ // from this flag on every render, and mountContent above renders
44732
+ // synchronously: flipping it any earlier commits an already-open DOM
44733
+ // (aria-expanded "true", navi-hidden gone) before openEffect has run
44734
+ // a single statement, so the "closed" frame it pins to transition
44735
+ // from is in fact the open one and the entrance animation has
44736
+ // nothing to play. It also gives the content it just built the
44737
+ // opening it is documented to observe — mounted while the popup
44738
+ // reads as closed, told it opened right after (see
44739
+ // popup_content_mount.js and use_displayed_layout_effect.js).
44740
+ controller.opened = true;
44741
+ const openEffectReturnValue =
44742
+ controller.openEffect(requestOpenEvent) || null;
44743
+ openEffectCleanup = (closeEvent) => {
44744
+ openEffectReturnValue?.(closeEvent);
44745
+ };
44746
+ closeHandlers = openHandler(requestOpenEvent) || null;
44747
+ controller.onOpenedChange?.(true, requestOpenEvent);
44748
+ },
44749
+ { opened: true, event: requestOpenEvent },
44750
+ );
44694
44751
  },
44695
44752
  requestClose: (
44696
44753
  e = new CustomEvent("programmatic", { detail: {} }),
44697
44754
  detail,
44698
44755
  ) => {
44756
+ flushChangeAwaitingTransition();
44699
44757
  if (!controller.opened) {
44700
44758
  return true;
44701
44759
  }
@@ -44718,6 +44776,7 @@ const createOpenController = (
44718
44776
  return true;
44719
44777
  },
44720
44778
  close: (e = new CustomEvent("programmatic", { detail: {} }), detail) => {
44779
+ flushChangeAwaitingTransition();
44721
44780
  if (!controller.opened) {
44722
44781
  return;
44723
44782
  }
@@ -44768,6 +44827,11 @@ const useOpenController = (openHandler) => {
44768
44827
  // no choice to leave open — close it for real.
44769
44828
  useLayoutEffect(() => {
44770
44829
  return () => {
44830
+ // Nothing to photograph on the way out: the popup is leaving the
44831
+ // document, so a movement between its box and anything else would be
44832
+ // played on an element already detached by the time the browser gets to
44833
+ // it (see controller.transitionChange).
44834
+ controllerRef.current.transitionChange = null;
44771
44835
  controllerRef.current.close();
44772
44836
  };
44773
44837
  }, []);
@@ -56710,6 +56774,146 @@ const translateOf = (axis, distance) =>
56710
56774
  ? `translate(${distance}px, 0px)`
56711
56775
  : `translate(0px, ${distance}px)`;
56712
56776
 
56777
+ /**
56778
+ * A popup that is the anchor, continued.
56779
+ *
56780
+ * Opening a surface out of the element that asked for it is not the surface
56781
+ * appearing while that element stays put: it is one box becoming another. The
56782
+ * browser draws exactly that on its own — the same `view-transition-name` on
56783
+ * the anchor before the change and on the popup after it, and it morphs the
56784
+ * first box into the second — so all this file does is hand that name over at
56785
+ * the two instants it can be handed over, and give it back afterwards.
56786
+ *
56787
+ * The pictures are taken around a DOM change, which is why the movement cannot
56788
+ * be written from outside: what a caller can reach — a command, `onClose` — is
56789
+ * either before the change or after it, never around it. The change that opens
56790
+ * a popup, and above all the one that closes it, belongs to the open
56791
+ * controller, which is what `transitionChange` (open_controller.js) exists to
56792
+ * let a popup wrap.
56793
+ *
56794
+ * One name serves the whole movement, because only one of the two boxes is on
56795
+ * screen at a time: it names the anchor while the popup is closed, and the
56796
+ * popup while it is open. What continues the anchor INSIDE the popup is
56797
+ * whatever carries `data-grow`, and the popup itself when nothing does — a
56798
+ * dialog holding the grown card plus buttons around it grows the card, a
56799
+ * dialog that IS the grown thing grows whole.
56800
+ *
56801
+ * The page around stays out of the picture (`view-transition-name: none` on
56802
+ * the root, in dialog.jsx): a captured element is not painted where it stands
56803
+ * and cannot be pointed at either, so photographing the whole document would
56804
+ * leave the page frozen and unpressable for the length of every opening — and
56805
+ * of every closing, which is the moment the user is coming back to it.
56806
+ */
56807
+
56808
+
56809
+ // The name the two boxes take turns wearing. A single literal one is enough,
56810
+ // and unique by construction: a document has one view transition, so it has at
56811
+ // most one popup growing, and the movement being replaced gives the name back
56812
+ // before the next one takes it (see releaseGrowInProgress).
56813
+ const NAME = "navi-popup-grow";
56814
+ const NAME_PROPERTY = "view-transition-name";
56815
+ // Worn by the root for the length of the movement — what the CSS keys the
56816
+ // page's own opt-out on, and the movement's only trace in the document.
56817
+ const ROOT_ATTRIBUTE = "data-navi-popup-grow";
56818
+ // Inside the popup, the one node that IS the anchor once it has grown.
56819
+ const TARGET_SELECTOR = "[data-grow]";
56820
+ // The popup's own animation duration, published on the root because the
56821
+ // ::view-transition tree hangs off it and inherits from nowhere else.
56822
+ const DURATION_PROPERTY = "--navi-popup-grow-duration";
56823
+
56824
+ let releaseGrowInProgress = null;
56825
+
56826
+ /**
56827
+ * Runs `applyChange` — the DOM change that opens or closes `popupEl` — inside
56828
+ * a view transition morphing the anchor's box into the popup's, or back.
56829
+ *
56830
+ * `opened` says which way: the box being left is the anchor when the popup is
56831
+ * opening and the popup when it is closing, and the arriving one is only known
56832
+ * once the change has been made (the content a popup grows into is built by
56833
+ * that very change).
56834
+ */
56835
+ const growPopupFromAnchor = (
56836
+ popupEl,
56837
+ anchorElement,
56838
+ applyChange,
56839
+ { opened },
56840
+ ) => {
56841
+ const startViewTransition = ensureDocumentStartViewTransition();
56842
+ // A movement still wearing the name would make the name two elements wide,
56843
+ // and a name belonging to two elements aborts the transition for the whole
56844
+ // document.
56845
+ releaseGrowInProgress?.();
56846
+
56847
+ const elementLeaving = opened ? anchorElement : resolveGrowTarget(popupEl);
56848
+ const giveBackNameLeaving = wearGrowName(elementLeaving);
56849
+ const root = document.documentElement;
56850
+ root.setAttribute(ROOT_ATTRIBUTE, "");
56851
+ const duration = getComputedStyle(popupEl)
56852
+ .getPropertyValue("--popup-animation-duration")
56853
+ .trim();
56854
+ if (duration) {
56855
+ // Empty would substitute into `animation-duration:` as nothing at all,
56856
+ // which computes to 0s — a movement nobody sees rather than one at the
56857
+ // browser's own pace.
56858
+ root.style.setProperty(DURATION_PROPERTY, duration);
56859
+ }
56860
+
56861
+ let giveBackNameArriving = null;
56862
+ const release = () => {
56863
+ if (releaseGrowInProgress !== release) {
56864
+ return;
56865
+ }
56866
+ releaseGrowInProgress = null;
56867
+ giveBackNameLeaving();
56868
+ giveBackNameArriving?.();
56869
+ root.removeAttribute(ROOT_ATTRIBUTE);
56870
+ root.style.removeProperty(DURATION_PROPERTY);
56871
+ };
56872
+ releaseGrowInProgress = release;
56873
+
56874
+ const viewTransition = startViewTransition(() => {
56875
+ // The name is the arriving box's from here on: worn by both, it is worn by
56876
+ // neither. Written rather than removed, so a name the element also has
56877
+ // from a stylesheet cannot resurface for the length of the movement.
56878
+ elementLeaving.style.setProperty(NAME_PROPERTY, "none");
56879
+ applyChange();
56880
+ const elementArriving = opened
56881
+ ? resolveGrowTarget(popupEl)
56882
+ : // Gone from the document while the popup was open (the row it stood
56883
+ // in was removed): nothing to arrive at, and the browser plays the
56884
+ // popup's picture out on its own.
56885
+ anchorElement.isConnected
56886
+ ? anchorElement
56887
+ : null;
56888
+ if (elementArriving) {
56889
+ giveBackNameArriving = wearGrowName(elementArriving);
56890
+ }
56891
+ });
56892
+ viewTransition.finished.then(release, release);
56893
+ };
56894
+
56895
+ const resolveGrowTarget = (popupEl) => {
56896
+ const declaredTarget = popupEl.querySelector(TARGET_SELECTOR);
56897
+ if (declaredTarget) {
56898
+ return declaredTarget;
56899
+ }
56900
+ return popupEl;
56901
+ };
56902
+
56903
+ // Wears the movement's name, and gives back whatever the element had written
56904
+ // inline of its own once the movement is over.
56905
+ const wearGrowName = (element) => {
56906
+ const nameBefore = element.style.getPropertyValue(NAME_PROPERTY);
56907
+ element.style.setProperty(NAME_PROPERTY, NAME);
56908
+ return () => {
56909
+ if (nameBefore) {
56910
+ element.style.setProperty(NAME_PROPERTY, nameBefore);
56911
+ return;
56912
+ }
56913
+ element.style.removeProperty(NAME_PROPERTY);
56914
+ };
56915
+ };
56916
+
56713
56917
  const PopupClose = ({
56714
56918
  label,
56715
56919
  iconSize,
@@ -56759,12 +56963,14 @@ installImportMetaCssBuild(import.meta);/**
56759
56963
  * popup_shared.js), even though several combinations land identically here
56760
56964
  * since Dialog is never really anchored — kept distinct anyway because
56761
56965
  * `positionArea` still picks which animation direction plays. `anchor` is
56762
- * inert here unless `sizeFromAnchor` asks for it: a dialog is a surface of
56763
- * its own, sized by its content, not a panel grown out of the control that
56764
- * opened it — that is Popover's job. With `sizeFromAnchor`, the anchor's box
56765
- * reaches the `--anchor-width`/`--anchor-height` CSS vars and becomes a
56766
- * min-width/min-height floor. Either way Dialog's own positioning is never
56767
- * relative to the anchor, unlike Popover.
56966
+ * inert here unless something asks for it: a dialog is a surface of its own,
56967
+ * sized by its content, not a panel grown out of the control that opened it —
56968
+ * that is Popover's job. Two props ask. With `sizeFromAnchor`, the anchor's
56969
+ * box reaches the `--anchor-width`/`--anchor-height` CSS vars and becomes a
56970
+ * min-width/min-height floor. With `animation="growing"`, the anchor's box is
56971
+ * where the dialog comes from: the two are photographed and morphed into each
56972
+ * other, and back on close (popup_grow.js). Either way Dialog's own
56973
+ * positioning is never relative to the anchor, unlike Popover.
56768
56974
  *
56769
56975
  * Two rendering strategies, picked via `layer`: `DialogAsModal` (a real
56770
56976
  * `<dialog>`, `showModal()`, top layer — native focus trap,
@@ -57209,6 +57415,27 @@ const css$E = /* css */`
57209
57415
  }
57210
57416
  }
57211
57417
 
57418
+ /* While a dialog is growing out of the element that opened it
57419
+ (popup_grow.js). The page around is deliberately NOT taken as a picture,
57420
+ against the browser's own default: a captured element is not painted where
57421
+ it stands and cannot be pointed at either, so photographing the document
57422
+ would leave the whole page frozen and unpressable for the length of every
57423
+ opening — and of every closing, which is the moment the user is coming
57424
+ back to it. Nothing shows through where the movement is: the two boxes it
57425
+ plays between are captured, and their pictures cover the rectangle between
57426
+ them at every moment. */
57427
+ :root[data-navi-popup-grow] {
57428
+ view-transition-name: none;
57429
+
57430
+ /* The popup's own pace, published on the root by popup_grow.js because the
57431
+ ::view-transition tree hangs off it and inherits from nowhere else. */
57432
+ &::view-transition-group(navi-popup-grow),
57433
+ &::view-transition-old(navi-popup-grow),
57434
+ &::view-transition-new(navi-popup-grow) {
57435
+ animation-duration: var(--navi-popup-grow-duration, 0.25s);
57436
+ }
57437
+ }
57438
+
57212
57439
  ${surfaceTextCss}
57213
57440
  ${popupCss}
57214
57441
  `;
@@ -57302,16 +57529,25 @@ const css$E = /* css */`
57302
57529
  * scroll while open (its backdrop only covers the scrollport, so scrolling
57303
57530
  * there would reveal uncovered content); this prop extends the lock to the
57304
57531
  * whole page. Defaults to `true` for a dialog docked by `dockedOnSmallTouchScreen`.
57305
- * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`} [props.animation]
57532
+ * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|"growing"|`slide-from-${string}`} [props.animation]
57306
57533
  * - `true`/`"auto"` resolves to `"scaling"` for a centered `positionArea`,
57307
57534
  * or a concrete `"slide-from-*"` direction otherwise. Any other explicit
57308
- * value is used as-is.
57535
+ * value is used as-is. `"growing"` is the odd one out: every other kind
57536
+ * moves the dialog on its own box (it scales from its own centre, slides in
57537
+ * from its own edge), while this one is the browser morphing the ANCHOR's
57538
+ * box into the dialog's, and the dialog's back into the anchor's on close —
57539
+ * for a dialog that is what the anchor became rather than a surface shown
57540
+ * over it. It needs an anchor (whatever opened the dialog, or the `anchor`
57541
+ * prop) and grows into whatever inside the dialog carries `data-grow`, the
57542
+ * dialog itself when nothing does. `"auto"` never picks it: only the caller
57543
+ * knows the two boxes are one object. See `popup_grow.js`.
57309
57544
  * @param {string} [props.animationDuration] - Maps to
57310
57545
  * `--popup-animation-duration`.
57311
57546
  * @param {Element|{current: Element}|string} [props.anchor] - Never used for
57312
57547
  * positioning (see this file's top comment), and ignored entirely unless
57313
- * `sizeFromAnchor` is set — then it sizes the dialog via the
57314
- * `--anchor-width`/`--anchor-height` CSS vars. Defaults to whatever
57548
+ * `sizeFromAnchor` or `animation="growing"` asks for it — the first sizes
57549
+ * the dialog via the `--anchor-width`/`--anchor-height` CSS vars, the second
57550
+ * makes the dialog come out of the anchor's own box. Defaults to whatever
57315
57551
  * triggered the open (`e.detail.anchor`), if any. A string is resolved via
57316
57552
  * `document.getElementById` when the dialog opens — see popover.jsx's own
57317
57553
  * `anchor` doc for why (mainly `defaultOpen`).
@@ -57708,6 +57944,8 @@ const useDialogProps = props => {
57708
57944
  marginWithContainer
57709
57945
  };
57710
57946
  const repositionRef = useRef(null);
57947
+ // The element this dialog came out of, held from the opening to the closing.
57948
+ const anchorElementRef = useRef(null);
57711
57949
  useEffect(() => {
57712
57950
  repositionRef.current?.(new CustomEvent("position_props_change", {
57713
57951
  detail: {}
@@ -57766,20 +58004,84 @@ const useDialogProps = props => {
57766
58004
  flushEdges.right = expandX || x === "right" || x === "inset-right";
57767
58005
  }
57768
58006
  const isAutoAnimation = animation === true || animation === "auto";
57769
- // Dialog never has a real anchor (see this file's top comment), so this
57770
- // is always the "no anchor" path — the same one Popover's own custom
57771
- // renderer falls into when it has no real anchor either.
58007
+ // The dialog and the anchor are one box, and what plays between them is the
58008
+ // browser's own morph (popup_grow.js) — nothing this dialog does to its own
58009
+ // box. So it arms no CSS transition of its own, which is not merely useless
58010
+ // here but wrong: the shared [navi-animation] rule transitions `display`
58011
+ // with allow-discrete, and a dialog kept rendered for the length of its exit
58012
+ // is exactly what the picture taken of the state it closes into must not
58013
+ // show.
58014
+ const growing = animation === "growing";
58015
+ // Dialog never has a real anchor to POSITION against (see this file's top
58016
+ // comment), so this is always the "no anchor" path — the same one Popover's
58017
+ // own custom renderer falls into when it has no real anchor either.
57772
58018
  const resolvedAnimationKind = isAutoAnimation ? resolveAutoAnimationKind(undefined, parsedPositionArea) : animation;
57773
58019
  // Not gated on isAutoAnimation — an explicit animation="sliding" needs a
57774
58020
  // concrete direction just as much as an auto-resolved one does (same as
57775
58021
  // Popover's own "sliding"/"expanding" resolution step in openEffect).
57776
- let resolvedAnimation = resolvedAnimationKind;
58022
+ let resolvedAnimation = growing ? undefined : resolvedAnimationKind;
57777
58023
  if (resolvedAnimationKind === "sliding") {
57778
58024
  resolvedAnimation = resolveDirectionValue(parsedPositionArea.y, parsedPositionArea.x, {
57779
58025
  prefix: "slide-from"
57780
58026
  }) ?? "slide-from-top";
57781
58027
  }
57782
58028
 
58029
+ // What this dialog was opened out of, resolved at open time rather than at
58030
+ // render time — mainly for defaultOpen, where there's no triggering
58031
+ // event/ref yet to read a real element from, only an id known up front. See
58032
+ // popover.jsx's own anchor handling for the full reasoning, mirrored here
58033
+ // identically.
58034
+ const resolveAnchorElement = e => {
58035
+ if (typeof anchor === "string") {
58036
+ const anchorElementById = document.getElementById(anchor);
58037
+ if (!anchorElementById) {
58038
+ console.warn(`Dialog: anchor="${anchor}" did not match any element`);
58039
+ }
58040
+ return anchorElementById;
58041
+ }
58042
+ if (anchor) {
58043
+ // anchor prop is a ref or a DOM element — always a real anchor,
58044
+ // regardless of anchorCustomEventDetail. A ref is unwrapped even when it
58045
+ // holds nothing: falling back to the ref object itself would pass the
58046
+ // "is there an anchor?" tests downstream with something that has no box.
58047
+ return "current" in anchor ? anchor.current : anchor;
58048
+ }
58049
+ if (anchorCustomEventDetail === "override") {
58050
+ // e.g. the button that triggered a --navi-toggle/--navi-open command,
58051
+ // already resolved from detail.anchor/detail.source by the caller
58052
+ // (see UncontrolledDialog's onnavi_request_open).
58053
+ return e.detail.anchor;
58054
+ }
58055
+ return undefined;
58056
+ };
58057
+
58058
+ // The dialog and the anchor are the same box at two sizes, so the opening
58059
+ // and the closing are one becoming the other. The browser draws that itself
58060
+ // provided the change happens between its two pictures, which is what
58061
+ // handing it to the controller buys (see popup_grow.js and
58062
+ // open_controller.js's own transitionChange).
58063
+ openController.transitionChange = growing ? (applyChange, {
58064
+ opened,
58065
+ event
58066
+ }) => {
58067
+ const dialogEl = ref.current;
58068
+ // A mount-time opening was never seen closed (see openEffect's own
58069
+ // `silent`): there is no box it comes from, because nothing was shown
58070
+ // before it.
58071
+ if (!dialogEl || opened && event.detail.silent) {
58072
+ applyChange();
58073
+ return;
58074
+ }
58075
+ const anchorElement = opened ? resolveAnchorElement(event) : anchorElementRef.current;
58076
+ if (!anchorElement) {
58077
+ applyChange();
58078
+ return;
58079
+ }
58080
+ growPopupFromAnchor(dialogEl, anchorElement, applyChange, {
58081
+ opened
58082
+ });
58083
+ } : null;
58084
+
57783
58085
  // Sync the DOM open and return how to sync it back closed, fresh on every
57784
58086
  // render so it closes over the latest props (scrollLock, etc.). The
57785
58087
  // controller (owned by the caller, or by UncontrolledDialog) decides
@@ -57806,28 +58108,10 @@ const useDialogProps = props => {
57806
58108
  // real, meaningful ancestor beyond it.
57807
58109
  const positionedAncestor = isModal ? document.documentElement : getPositionedParent(dialogEl.parentElement /* dialogEl is inside the clip_wrapper */);
57808
58110
  const [cleanup, addCleanup] = createPubSub(true);
57809
- let anchorElement;
57810
- if (typeof anchor === "string") {
57811
- // Resolved at open time (not render time) via getElementById — mainly
57812
- // for defaultOpen, where there's no triggering event/ref yet to read a
57813
- // real element from, only an id known up front. See popover.jsx's own
57814
- // anchor handling for the full reasoning, mirrored here identically.
57815
- anchorElement = document.getElementById(anchor);
57816
- if (!anchorElement) {
57817
- console.warn(`Dialog: anchor="${anchor}" did not match any element`);
57818
- }
57819
- } else if (anchor) {
57820
- // anchor prop is a ref or a DOM element — always a real anchor,
57821
- // regardless of anchorCustomEventDetail. A ref is unwrapped even when it
57822
- // holds nothing: falling back to the ref object itself would pass the
57823
- // "is there an anchor?" test below with something that has no box.
57824
- anchorElement = "current" in anchor ? anchor.current : anchor;
57825
- } else if (anchorCustomEventDetail === "override") {
57826
- // e.g. the button that triggered a --navi-toggle/--navi-open command,
57827
- // already resolved from detail.anchor/detail.source by the caller
57828
- // (see UncontrolledDialog's onnavi_request_open).
57829
- anchorElement = e.detail.anchor;
57830
- }
58111
+ const anchorElement = resolveAnchorElement(e);
58112
+ // Kept for the closing, which has no opening event to read it back from:
58113
+ // the box the dialog shrinks back into is the one it came out of.
58114
+ anchorElementRef.current = anchorElement;
57831
58115
  debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> openDialog`);
57832
58116
  if (!isModal) {
57833
58117
  // see openLocalDialogCount's own comment