@jsenv/navi 0.29.22 → 0.29.24

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.
@@ -25727,6 +25727,39 @@ const popupCss = /* css */ `
25727
25727
  }
25728
25728
  `;
25729
25729
 
25730
+ /**
25731
+ * Holding a box at the size it has right now — what the `sizing="frozen"` prop
25732
+ * on Dialog, Popover and SlideContainer is made of.
25733
+ *
25734
+ * The need: a surface being used is a frame that has been put down. What moves
25735
+ * inside it is its content, not the frame — a list one empties by acting on it
25736
+ * (marking as read, archiving) must not resize the box under the finger, or the
25737
+ * next row moves while it is being aimed at.
25738
+ */
25739
+
25740
+ /**
25741
+ * Read back through getComputedStyle rather than offsetWidth/offsetHeight: the
25742
+ * used values honour whatever `box-sizing` is in effect, so writing them back
25743
+ * reproduces exactly the box that was measured, to the subpixel. Neither is
25744
+ * affected by a transform, so this is safe to call while an entrance animation
25745
+ * is scaling the box.
25746
+ *
25747
+ * `width`/`height`, never `min-width`/`min-height`: a `max-*` — the caller's
25748
+ * own, or the container ceiling a popup already computes for itself — must keep
25749
+ * winning, so a box frozen at 500px on a phone held upright still fits once it
25750
+ * is turned.
25751
+ */
25752
+ const freezeSize = (el) => {
25753
+ const { width, height } = getComputedStyle(el);
25754
+ el.style.width = width;
25755
+ el.style.height = height;
25756
+ };
25757
+
25758
+ const unfreezeSize = (el) => {
25759
+ el.style.width = "";
25760
+ el.style.height = "";
25761
+ };
25762
+
25730
25763
  /**
25731
25764
  * Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
25732
25765
  * (non-top-layer) renderers — operate on a plain DOM element, no knowledge
@@ -26311,6 +26344,19 @@ const css$V = /* css */`
26311
26344
  * @param {string} [props.minHeight] - Maps to `--dialog-min-height`, same
26312
26345
  * clamping as `minWidth`.
26313
26346
  * @param {string} [props.maxHeight] - Maps to `--dialog-max-height`.
26347
+ * @param {"auto"|"frozen"} [props.sizing="auto"] - `"auto"`: the dialog follows
26348
+ * its content for as long as it stays open. `"frozen"`: it is measured once
26349
+ * and held at that size until it closes — what no longer fits (or no longer
26350
+ * fills it) is the scroll's business. For a surface acted upon while it is
26351
+ * open: marking a notification as read, emptying a queue, swapping between
26352
+ * two slides of different heights — the row being aimed at must not move
26353
+ * under the finger. The measure is taken at the first render where this says
26354
+ * `"frozen"`, so a dialog opening on skeletons can say
26355
+ * `sizing={loading ? "auto" : "frozen"}` and be measured once the real
26356
+ * content is there. The freeze writes a `height`/`width`, never a `min-*`:
26357
+ * `maxHeight`/`maxWidth` and the container ceiling keep winning, so a frozen
26358
+ * dialog still fits when the phone is turned. Closing releases it — the next
26359
+ * opening measures again.
26314
26360
  * @param {number} [props.tabIndex=-1] - Set on the dialog element itself so
26315
26361
  * `autoFocus="last-resort"` below has somewhere to land when the dialog has
26316
26362
  * no other focusable descendant of its own.
@@ -26515,6 +26561,9 @@ const useDialogProps = props => {
26515
26561
  // actually makes "capture"/"none" behave the same way here too.
26516
26562
  pointerInteractionOutsideEffect = "close",
26517
26563
  scrollCapture: scrollCaptureProp,
26564
+ // "auto" (default) → the dialog follows its content. "frozen" → measured
26565
+ // once, held at that size while open. See this prop's own JSDoc above.
26566
+ sizing = "auto",
26518
26567
  animation,
26519
26568
  // Only ever affects --anchor-width/--anchor-height (see this file's top
26520
26569
  // comment) — Dialog's own positioning is never relative to it.
@@ -26577,6 +26626,21 @@ const useDialogProps = props => {
26577
26626
  detail: {}
26578
26627
  }));
26579
26628
  }, [positionArea, marginWithContainer]);
26629
+ // The freeze is taken where the value changes, not only at open time: a
26630
+ // dialog showing skeletons first says sizing="auto" until its content is
26631
+ // there, and would otherwise be held at the size of the waiting state.
26632
+ // Opening while already "frozen" is openEffect's own case.
26633
+ useEffect(() => {
26634
+ const dialogEl = ref.current;
26635
+ if (!dialogEl || !openController.opened) {
26636
+ return;
26637
+ }
26638
+ if (sizing === "frozen") {
26639
+ freezeSize(dialogEl);
26640
+ } else {
26641
+ unfreezeSize(dialogEl);
26642
+ }
26643
+ }, [sizing]);
26580
26644
  const positionAreaParseResult = parsePositionArea(positionArea);
26581
26645
  if (!positionAreaParseResult) {
26582
26646
  console.warn(`Dialog: invalid positionArea="${positionArea}"`);
@@ -26808,6 +26872,13 @@ const useDialogProps = props => {
26808
26872
  // navi_position_change on every call) — nothing to do here.
26809
26873
  };
26810
26874
  positionDialog();
26875
+ if (sizing === "frozen") {
26876
+ // After positionDialog: the caps it writes
26877
+ // (--container-position-remaining-*) are part of what decides the size
26878
+ // being taken, so measuring before it would freeze a box the dialog
26879
+ // never actually had.
26880
+ freezeSize(dialogEl);
26881
+ }
26811
26882
 
26812
26883
  // Reposition on the same triggers Popover's own visibleRectEffect
26813
26884
  // already reacts to generically — window resize/scroll/visual-viewport
@@ -26939,6 +27010,9 @@ const useDialogProps = props => {
26939
27010
  // property is actually present — harmless the rest of the time.
26940
27011
  dialogEl.setAttribute("navi-hidden", "");
26941
27012
  dialogEl.close();
27013
+ // The freeze only ever holds for one opening: the next one has its own
27014
+ // content to be measured against.
27015
+ unfreezeSize(dialogEl);
26942
27016
  cancelOpenInteractionSuppression?.();
26943
27017
  if (hasCssTransitionAnimation) {
26944
27018
  suppressPointerEventsDuringTransition(dialogEl);
@@ -27516,6 +27590,18 @@ const css$U = /* css */`
27516
27590
  * @param {string} [props.minHeight] - Maps to `--popover-min-height`, same
27517
27591
  * clamping as `minWidth`.
27518
27592
  * @param {string} [props.maxHeight] - Maps to `--popover-max-height`.
27593
+ * @param {"auto"|"frozen"} [props.sizing="auto"] - `"auto"`: the popover
27594
+ * follows its content for as long as it stays open. `"frozen"`: it is
27595
+ * measured once and held at that size until it closes — what no longer fits
27596
+ * (or no longer fills it) is the scroll's business. For a surface acted upon
27597
+ * while it is open: emptying a list, swapping between two panels of
27598
+ * different heights — the row being aimed at must not move under the
27599
+ * pointer. The measure is taken at the first render where this says
27600
+ * `"frozen"`, so a popover opening on skeletons can say
27601
+ * `sizing={loading ? "auto" : "frozen"}` and be measured once the real
27602
+ * content is there. The freeze writes a `height`/`width`, never a `min-*`:
27603
+ * `maxHeight`/`maxWidth` and the container ceiling keep winning. Closing
27604
+ * releases it — the next opening measures again.
27519
27605
  * @param {number} [props.tabIndex=-1] - Set on the popover element itself
27520
27606
  * so `autoFocus="last-resort"` below has somewhere to land when the popover
27521
27607
  * has no other focusable descendant of its own.
@@ -27716,6 +27802,9 @@ const usePopoverProps = props => {
27716
27802
  pointerInteractionOutsideEffect = "none",
27717
27803
  scrollCapture,
27718
27804
  focusCapture,
27805
+ // "auto" (default) → the popover follows its content. "frozen" → measured
27806
+ // once, held at that size while open. See this prop's own JSDoc above.
27807
+ sizing = "auto",
27719
27808
  animation,
27720
27809
  anchor,
27721
27810
  anchorCustomEventDetail = "override",
@@ -27774,6 +27863,21 @@ const usePopoverProps = props => {
27774
27863
  detail: {}
27775
27864
  }));
27776
27865
  }, [positionArea, positionAreaFixed, positionAreaWhenAnchorIsInvalid, marginWithAnchor, marginWithContainer]);
27866
+ // The freeze is taken where the value changes, not only at open time: a
27867
+ // popover showing skeletons first says sizing="auto" until its content is
27868
+ // there, and would otherwise be held at the size of the waiting state.
27869
+ // Opening while already "frozen" is openEffect's own case.
27870
+ useEffect(() => {
27871
+ const popoverEl = ref.current;
27872
+ if (!popoverEl || !openController.opened) {
27873
+ return;
27874
+ }
27875
+ if (sizing === "frozen") {
27876
+ freezeSize(popoverEl);
27877
+ } else {
27878
+ unfreezeSize(popoverEl);
27879
+ }
27880
+ }, [sizing]);
27777
27881
  // The custom renderer's own starting-hidden state is a stylesheet default
27778
27882
  // now (&:not([popover]) { display: none } on .navi_popover/
27779
27883
  // .navi_popover_backdrop above) rather than set here imperatively — a
@@ -28170,6 +28274,14 @@ const usePopoverProps = props => {
28170
28274
  addCleanup(() => {
28171
28275
  rectEffect.disconnect();
28172
28276
  });
28277
+ if (sizing === "frozen") {
28278
+ // After rectEffect's own setup, which has already placed the popover:
28279
+ // the caps that placement writes
28280
+ // (--container-position-remaining-*) are part of what decides the size
28281
+ // being taken, so measuring before it would freeze a box the popover
28282
+ // never actually had.
28283
+ freezeSize(popoverEl);
28284
+ }
28173
28285
 
28174
28286
  // "sliding"/"expanding" need a concrete direction (see
28175
28287
  // resolveDirectionValue) — resolved here, once, now that rectEffect's
@@ -28265,6 +28377,9 @@ const usePopoverProps = props => {
28265
28377
  } else {
28266
28378
  openLocalPopoverCount = Math.max(0, openLocalPopoverCount - 1);
28267
28379
  }
28380
+ // The freeze only ever holds for one opening: the next one has its own
28381
+ // content to be measured against.
28382
+ unfreezeSize(popoverEl);
28268
28383
  // Not interactive while it's leaving either — cancel the open side's
28269
28384
  // still-pending suppression first, since a fresh one below fully
28270
28385
  // replaces it (nothing ever needs to cancel this one in turn: a
@@ -45593,6 +45708,18 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
45593
45708
  * scrolling the document — so a map that travels vertically has to be told
45594
45709
  * (`travelByScroll` / `"y"` / `"xy"`) before a wheel moves it.
45595
45710
  * @param {string} [props.duration="300ms"] - how long a slide change takes.
45711
+ * @param {"largest"|"frozen"} [props.sizing="largest"] - what decides the size
45712
+ * of the box. "largest": the largest slide, at every moment — the box follows
45713
+ * whatever the slides do. "frozen": the box is measured once and kept at that
45714
+ * size, and what no longer fits (or no longer fills it) is the scroll's
45715
+ * business. For slides one ACTS on rather than merely reads: a row marked as
45716
+ * read leaves one panel for the other, the tallest slide is not the same one
45717
+ * anymore, and with "largest" the box resizes under the finger aiming at the
45718
+ * next row. The measure is taken at the first render where this says
45719
+ * "frozen", so a container opening on skeletons says
45720
+ * `sizing={loading ? "largest" : "frozen"}` and is measured once the real
45721
+ * content is there. The freeze holds against what happens INSIDE the box, not
45722
+ * against the room it is given: a window resize measures again.
45596
45723
  */
45597
45724
  const SlideContainer = ({
45598
45725
  layout = "row",
@@ -45606,6 +45733,7 @@ const SlideContainer = ({
45606
45733
  travelByDrag = true,
45607
45734
  travelByScroll = "x",
45608
45735
  duration = "300ms",
45736
+ sizing = "largest",
45609
45737
  children,
45610
45738
  ...rest
45611
45739
  }) => {
@@ -45740,6 +45868,31 @@ const SlideContainer = ({
45740
45868
  };
45741
45869
  }, [noTravel]);
45742
45870
 
45871
+ // The box, held at the size it has right now (sizing="frozen"): what the
45872
+ // slides do afterwards moves their own content and not this box. Taken where
45873
+ // the value changes rather than at mount alone, so a container opening on
45874
+ // skeletons is measured on the real thing (see the prop's own doc).
45875
+ useLayoutEffect(() => {
45876
+ if (sizing !== "frozen") {
45877
+ return undefined;
45878
+ }
45879
+ const containerEl = containerRef.current;
45880
+ freezeSize(containerEl);
45881
+ // The freeze is about the content, never about the room: a box frozen on a
45882
+ // phone held upright has to fit once it is turned, and one frozen in a
45883
+ // window has to follow that window. So the measure is taken again whenever
45884
+ // the room changes — the only moment the box is allowed to resize.
45885
+ const onWindowResize = () => {
45886
+ unfreezeSize(containerEl);
45887
+ freezeSize(containerEl);
45888
+ };
45889
+ window.addEventListener("resize", onWindowResize);
45890
+ return () => {
45891
+ window.removeEventListener("resize", onWindowResize);
45892
+ unfreezeSize(containerEl);
45893
+ };
45894
+ }, [sizing]);
45895
+
45743
45896
  // What the user was doing on each slide, so coming back comes back to it. The
45744
45897
  // ways out are left out on purpose: pressing one is how one LEAVES a slide,
45745
45898
  // and remembering it would mean coming back to the exit rather than to the
@@ -48151,6 +48304,10 @@ const css$z = /* css */`
48151
48304
  * @param {string} [props.minWidth] - Forwarded as-is.
48152
48305
  * @param {string} [props.minHeight] - Forwarded as-is.
48153
48306
  * @param {string} [props.maxHeight] - Forwarded as-is.
48307
+ * @param {"auto"|"frozen"} [props.sizing] - Forwarded as-is to both, which
48308
+ * understand it identically: `"frozen"` holds the surface at the size it was
48309
+ * measured at while it stays open, so acting on what it contains moves the
48310
+ * content and not the surface. See either component's own doc.
48154
48311
  * @param {boolean} [props.expand] - Dialog-mode only: shorthand for both
48155
48312
  * `expandX`/`expandY` below. No effect in popover mode.
48156
48313
  * @param {boolean} [props.expandX] - Dialog-mode only: stretches the dialog