@jsenv/dom 0.17.32 → 0.17.34

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.
Files changed (2) hide show
  1. package/dist/jsenv_dom.js +89 -28
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -4896,9 +4896,18 @@ const findFocusable = (element, { exclude } = {}) => {
4896
4896
  return focusableDescendant;
4897
4897
  };
4898
4898
 
4899
- // note: keep in mind that an element with overflow: 'hidden' is scrollable
4900
- // it can be scrolled using keyboard arrows or JavaScript properties such as scrollTop, scrollLeft
4901
- // the only overflow that prevents scroll is "visible"
4899
+ // `hidden` and `clip` both cut the content off and are two different things
4900
+ // here, which is what `includeHidden` is about:
4901
+ // - `hidden` IS a scroll container. Nothing scrolls it by hand, but keyboard
4902
+ // arrows and scrollTop/scrollLeft do, so whoever cares where an element can
4903
+ // be brought into view has to count it.
4904
+ // - `clip` is not one. It clips and stops there: it has no scroll box at all,
4905
+ // and scrollTop/scrollLeft on it read back 0 whatever they are set to. That
4906
+ // is the whole reason a layout reaches for it (see navi's
4907
+ // docs/MOBILE_LAYOUT_PITFALLS.md), so it is never included, whatever the
4908
+ // caller asks for.
4909
+ // `visible` is the other non-scrolling value, and the only one on which the
4910
+ // content still reaches the ancestors.
4902
4911
  const isScrollable = (element, { includeHidden } = {}) => {
4903
4912
  if (canHaveVerticalScroll(element, { includeHidden })) {
4904
4913
  return true;
@@ -4940,7 +4949,10 @@ const canHaveVerticalScroll = (element, { includeHidden }) => {
4940
4949
  }
4941
4950
  return false;
4942
4951
  }
4943
- if (verticalOverflow === "hidden" || verticalOverflow === "clip") {
4952
+ if (verticalOverflow === "clip") {
4953
+ return false;
4954
+ }
4955
+ if (verticalOverflow === "hidden") {
4944
4956
  return includeHidden;
4945
4957
  }
4946
4958
  const overflow = getStyle(element, "overflow");
@@ -4951,7 +4963,10 @@ const canHaveVerticalScroll = (element, { includeHidden }) => {
4951
4963
  }
4952
4964
  return false;
4953
4965
  }
4954
- if (overflow === "hidden" || overflow === "clip") {
4966
+ if (overflow === "clip") {
4967
+ return false;
4968
+ }
4969
+ if (overflow === "hidden") {
4955
4970
  return includeHidden;
4956
4971
  }
4957
4972
  return true; // "auto", "scroll"
@@ -4965,7 +4980,10 @@ const canHaveHorizontalScroll = (element, { includeHidden }) => {
4965
4980
  }
4966
4981
  return false;
4967
4982
  }
4968
- if (horizontalOverflow === "hidden" || horizontalOverflow === "clip") {
4983
+ if (horizontalOverflow === "clip") {
4984
+ return false;
4985
+ }
4986
+ if (horizontalOverflow === "hidden") {
4969
4987
  return includeHidden;
4970
4988
  }
4971
4989
  const overflow = getStyle(element, "overflow");
@@ -4976,7 +4994,10 @@ const canHaveHorizontalScroll = (element, { includeHidden }) => {
4976
4994
  }
4977
4995
  return false;
4978
4996
  }
4979
- if (overflow === "hidden" || overflow === "clip") {
4997
+ if (overflow === "clip") {
4998
+ return false;
4999
+ }
5000
+ if (overflow === "hidden") {
4980
5001
  return includeHidden;
4981
5002
  }
4982
5003
  return true; // "auto", "scroll"
@@ -15686,7 +15707,7 @@ const visibleRectEffect = (
15686
15707
  onNaviPositionChange,
15687
15708
  );
15688
15709
  // Dispatched by applyNewPosition's own notifyPositionTransition
15689
- // around this ancestor's own left/top animation (distinct from
15710
+ // around this ancestor's own placement animation (distinct from
15690
15711
  // navi_position_change, fired once with the final target, not per
15691
15712
  // frame). The anchor this element is positioned against may live
15692
15713
  // inside that ancestor and be moving right now — rather than hiding
@@ -16008,7 +16029,10 @@ const toContainerAlignedPosition = (value) => {
16008
16029
  * axis never reads the attribute back itself (`positionAreaFixed` always wins).
16009
16030
  *
16010
16031
  * @param {HTMLElement} element - The element to position (position: absolute or
16011
- * fixed — detected from its own computed style, see the scroll offset comment below)
16032
+ * fixed — detected from its own computed style, see the scroll offset comment below),
16033
+ * laid out at its containing block's own origin: the returned `left`/`top` are meant
16034
+ * to be applied as a translate from there, see `applyNewPosition` for the whole
16035
+ * contract and for why the placement may not go through `left`/`top` themselves
16012
16036
  * @param {HTMLElement} [anchor] - The anchor element to position against. Omit (or pass
16013
16037
  * `null`/`undefined`) when there's no real anchor to dock `element` against a *container*
16014
16038
  * instead — see `container` below; in that mode, "top"/"bottom"/"left"/"right" are
@@ -16206,12 +16230,12 @@ const pickPositionRelativeTo = (
16206
16230
  const clampLeftBound = availableLeft;
16207
16231
  const clampRightBound = availableRight;
16208
16232
  // offsetWidth/offsetHeight (layout box), not getBoundingClientRect() (the
16209
- // painted/transformed box): the element being positioned may have an
16210
- // active CSS `scale`/`translate` transform mid-animation (e.g. a popover
16211
- // using animation="scale"/"grow", still at its @starting-style value the
16212
- // instant it's first shown) — getBoundingClientRect() would then report
16213
- // its *shrunk* transformed size, throwing off any math that centers/fits
16214
- // against the element's own dimensions.
16233
+ // painted/transformed box): the element being positioned is moved by
16234
+ // transforms its own placement translate (see applyNewPosition), plus
16235
+ // whatever `scale` an entrance animation happens to be playing at that
16236
+ // instant (a popover using animation="scaling"/"expand-*") — so
16237
+ // getBoundingClientRect() answers where it is painted, at its *shrunk*
16238
+ // mid-animation size, instead of the box being measured here.
16215
16239
  const elementWidth = element.offsetWidth;
16216
16240
  const elementHeight = element.offsetHeight;
16217
16241
  const anchorWidth = anchorRight - anchorLeft;
@@ -16720,11 +16744,32 @@ const notifyPositionTransition = (element, animation) => {
16720
16744
  };
16721
16745
 
16722
16746
  /**
16723
- * Applies a `pickPositionRelativeTo` result to `element`. `left`/`top` are
16724
- * set instantly (a scroll-triggered reposition should never lag its
16725
- * target); when `shouldTransition` is set (a resize-triggered reposition),
16726
- * the visual move is played out via `element.animate()` instead — kept
16727
- * independent of Popover/Dialog/Callout's own opacity/scale/display CSS
16747
+ * Applies a `pickPositionRelativeTo` result to `element`, as a `translate`.
16748
+ *
16749
+ * The two halves of that contract, which a caller must honor:
16750
+ *
16751
+ * 1. `element` stays laid out at its containing block's own origin — its CSS
16752
+ * must say `left: 0; top: 0` (and leave right/bottom `auto`). The computed
16753
+ * `left`/`top` are then applied as a translate from there, which lands the
16754
+ * box in exactly the same place `left: Npx` would have. The reason to go
16755
+ * through a transform at all is the *measuring*: an out-of-flow box with a
16756
+ * shrink-to-fit width can never be wider than "containing block width -
16757
+ * left", so a box placed with `left` reports a width that depends on where
16758
+ * it currently stands — and `pickPositionRelativeTo` decides which side to
16759
+ * place it on from that very width. Feeding a placement back into the
16760
+ * decision that produced it makes the element alternate sides forever, one
16761
+ * flip per reposition. Laid out at the origin, the measured width only ever
16762
+ * depends on the content and on the size caps below.
16763
+ * 2. The `translate` property belongs to this function alone. An animation on
16764
+ * the same element uses `scale` (which composes: individual transform
16765
+ * properties apply translate, then rotate, then scale, then `transform`, so
16766
+ * a scale never rescales the placement) or `transform` — see popup_css.js,
16767
+ * whose slide entrances translate through `transform` for that reason.
16768
+ *
16769
+ * The translate is set instantly (a scroll-triggered reposition should never
16770
+ * lag its target); when `shouldTransition` is set (a resize-triggered
16771
+ * reposition), the visual move is played out via `element.animate()` instead —
16772
+ * kept independent of Popover/Dialog/Callout's own opacity/scale/display CSS
16728
16773
  * transition on the same element, so neither can clobber the other (see
16729
16774
  * notifyPositionTransition's own doc for why a dedicated Animation over a
16730
16775
  * CSS one). Duration comes from `--popup-position-transition-duration`
@@ -16796,18 +16841,19 @@ const applyNewPosition = (
16796
16841
  // A single implicit keyframe turned out not to work here: the WAAPI
16797
16842
  // "neutral" start keyframe isn't frozen at `animate()` call time, it's
16798
16843
  // resolved from the underlying value when the animation is first
16799
- // *sampled* (the next frame) — by then `element.style.left`/`top` below
16844
+ // *sampled* (the next frame) — by then `element.style.translate` below
16800
16845
  // has already been overwritten with the new target, so start === end and
16801
16846
  // nothing visibly moves (observed as the dialog just jumping). Reading
16802
16847
  // the previous value ourselves, before overwriting it, and passing both
16803
16848
  // keyframes explicitly sidesteps that entirely.
16804
- const previousLeft = parseFloat(element.style.left) || left;
16805
- const previousTop = parseFloat(element.style.top) || top;
16849
+ const previousTranslate = parseTranslate(element.style.translate);
16850
+ const previousLeft = previousTranslate ? previousTranslate.x : left;
16851
+ const previousTop = previousTranslate ? previousTranslate.y : top;
16806
16852
  if (shouldTransition) {
16807
16853
  const animation = element.animate(
16808
16854
  [
16809
- { left: `${previousLeft}px`, top: `${previousTop}px` },
16810
- { left: `${left}px`, top: `${top}px` },
16855
+ { translate: `${previousLeft}px ${previousTop}px` },
16856
+ { translate: `${left}px ${top}px` },
16811
16857
  ],
16812
16858
  {
16813
16859
  duration: parseTransitionDurationMs(
@@ -16820,16 +16866,31 @@ const applyNewPosition = (
16820
16866
  );
16821
16867
  notifyPositionTransition(element, animation);
16822
16868
  }
16823
- // The specified `left`/`top` are set to their final target right away,
16869
+ // The specified translate is set to its final target right away,
16824
16870
  // regardless of `shouldTransition` — the animation above only plays the
16825
16871
  // visual move from the old position, it never becomes the actual
16826
16872
  // specified style (see notifyPositionTransition's own commitStyles for
16827
16873
  // why that matters once it ends).
16828
- element.style.left = `${left}px`;
16829
- element.style.top = `${top}px`;
16874
+ element.style.translate = `${left}px ${top}px`;
16830
16875
  dispatchCustomEvent(element, "navi_position_change");
16831
16876
  };
16832
16877
 
16878
+ // "42px 100px" as { x, y }. Anything else — unset, "none", a single-value
16879
+ // shorthand — reads as "never placed yet" (null), so a first placement has no
16880
+ // stale point to be animated from.
16881
+ const parseTranslate = (translate) => {
16882
+ if (!translate) {
16883
+ return null;
16884
+ }
16885
+ const [x, y] = translate.split(" ");
16886
+ const xNumber = parseFloat(x);
16887
+ const yNumber = parseFloat(y);
16888
+ if (Number.isNaN(xNumber) || Number.isNaN(yNumber)) {
16889
+ return null;
16890
+ }
16891
+ return { x: xNumber, y: yNumber };
16892
+ };
16893
+
16833
16894
  const [publishDebugger, subscribeDebugger] = createPubSub();
16834
16895
 
16835
16896
  const notifyDebuggerStart = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.32",
3
+ "version": "0.17.34",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {