@jsenv/dom 0.17.31 → 0.17.33

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 +119 -27
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -15008,17 +15008,69 @@ window.addEventListener("resize", (event) => {
15008
15008
  const getVisibleViewportRect = () => {
15009
15009
  const visualViewport = window.visualViewport;
15010
15010
  const documentElement = document.documentElement;
15011
- const height = visualViewport
15012
- ? visualViewport.height
15013
- : documentElement.clientHeight;
15011
+ if (!visualViewport) {
15012
+ return {
15013
+ left: 0,
15014
+ top: 0,
15015
+ width: documentElement.clientWidth,
15016
+ height: Math.max(
15017
+ 0,
15018
+ documentElement.clientHeight - getVirtualKeyboardOverlayHeight(),
15019
+ ),
15020
+ };
15021
+ }
15022
+ let { offsetLeft, offsetTop, width, height } = visualViewport;
15023
+ // The visual viewport only drifts from the layout viewport while the page is
15024
+ // pinch-zoomed or an on-screen keyboard is up (Safari/Firefox shrink and
15025
+ // pan it to keep the field in view). Neither can be the case at scale 1
15026
+ // with nothing editable focused — an on-screen keyboard is dismissed by
15027
+ // blurring the field — so an offset reported then is a stale one. iOS 26.0
15028
+ // keeps the last keyboard offset and height after the keyboard went away
15029
+ // (https://developer.apple.com/forums/thread/800125): trusted as-is, that
15030
+ // offset sends a bottom-docked sheet past the bottom edge, out of sight.
15031
+ if (
15032
+ visualViewport.scale === 1 &&
15033
+ !isEditableElement(document.activeElement)
15034
+ ) {
15035
+ offsetLeft = 0;
15036
+ offsetTop = 0;
15037
+ if (window.innerHeight > height) {
15038
+ height = window.innerHeight;
15039
+ }
15040
+ }
15014
15041
  return {
15015
- left: visualViewport ? visualViewport.offsetLeft : 0,
15016
- top: visualViewport ? visualViewport.offsetTop : 0,
15017
- width: visualViewport ? visualViewport.width : documentElement.clientWidth,
15042
+ left: offsetLeft,
15043
+ top: offsetTop,
15044
+ width,
15018
15045
  height: Math.max(0, height - getVirtualKeyboardOverlayHeight()),
15019
15046
  };
15020
15047
  };
15021
15048
 
15049
+ const isEditableElement = (element) => {
15050
+ if (!element) {
15051
+ return false;
15052
+ }
15053
+ if (element.tagName === "TEXTAREA" || element.isContentEditable) {
15054
+ return true;
15055
+ }
15056
+ if (element.tagName === "INPUT") {
15057
+ return !NON_TYPING_INPUT_TYPES.has(element.type);
15058
+ }
15059
+ return false;
15060
+ };
15061
+ const NON_TYPING_INPUT_TYPES = new Set([
15062
+ "button",
15063
+ "checkbox",
15064
+ "color",
15065
+ "file",
15066
+ "hidden",
15067
+ "image",
15068
+ "radio",
15069
+ "range",
15070
+ "reset",
15071
+ "submit",
15072
+ ]);
15073
+
15022
15074
  // Minimum fraction of element width/height that must be visible on the preferred side
15023
15075
  // before flipping to the opposite side. Prevents flickering near the flip threshold.
15024
15076
  const MIN_CONTENT_VISIBILITY_RATIO = 0.6;
@@ -15634,7 +15686,7 @@ const visibleRectEffect = (
15634
15686
  onNaviPositionChange,
15635
15687
  );
15636
15688
  // Dispatched by applyNewPosition's own notifyPositionTransition
15637
- // around this ancestor's own left/top animation (distinct from
15689
+ // around this ancestor's own placement animation (distinct from
15638
15690
  // navi_position_change, fired once with the final target, not per
15639
15691
  // frame). The anchor this element is positioned against may live
15640
15692
  // inside that ancestor and be moving right now — rather than hiding
@@ -15956,7 +16008,10 @@ const toContainerAlignedPosition = (value) => {
15956
16008
  * axis never reads the attribute back itself (`positionAreaFixed` always wins).
15957
16009
  *
15958
16010
  * @param {HTMLElement} element - The element to position (position: absolute or
15959
- * fixed — detected from its own computed style, see the scroll offset comment below)
16011
+ * fixed — detected from its own computed style, see the scroll offset comment below),
16012
+ * laid out at its containing block's own origin: the returned `left`/`top` are meant
16013
+ * to be applied as a translate from there, see `applyNewPosition` for the whole
16014
+ * contract and for why the placement may not go through `left`/`top` themselves
15960
16015
  * @param {HTMLElement} [anchor] - The anchor element to position against. Omit (or pass
15961
16016
  * `null`/`undefined`) when there's no real anchor to dock `element` against a *container*
15962
16017
  * instead — see `container` below; in that mode, "top"/"bottom"/"left"/"right" are
@@ -16154,12 +16209,12 @@ const pickPositionRelativeTo = (
16154
16209
  const clampLeftBound = availableLeft;
16155
16210
  const clampRightBound = availableRight;
16156
16211
  // offsetWidth/offsetHeight (layout box), not getBoundingClientRect() (the
16157
- // painted/transformed box): the element being positioned may have an
16158
- // active CSS `scale`/`translate` transform mid-animation (e.g. a popover
16159
- // using animation="scale"/"grow", still at its @starting-style value the
16160
- // instant it's first shown) — getBoundingClientRect() would then report
16161
- // its *shrunk* transformed size, throwing off any math that centers/fits
16162
- // against the element's own dimensions.
16212
+ // painted/transformed box): the element being positioned is moved by
16213
+ // transforms its own placement translate (see applyNewPosition), plus
16214
+ // whatever `scale` an entrance animation happens to be playing at that
16215
+ // instant (a popover using animation="scaling"/"expand-*") — so
16216
+ // getBoundingClientRect() answers where it is painted, at its *shrunk*
16217
+ // mid-animation size, instead of the box being measured here.
16163
16218
  const elementWidth = element.offsetWidth;
16164
16219
  const elementHeight = element.offsetHeight;
16165
16220
  const anchorWidth = anchorRight - anchorLeft;
@@ -16668,11 +16723,32 @@ const notifyPositionTransition = (element, animation) => {
16668
16723
  };
16669
16724
 
16670
16725
  /**
16671
- * Applies a `pickPositionRelativeTo` result to `element`. `left`/`top` are
16672
- * set instantly (a scroll-triggered reposition should never lag its
16673
- * target); when `shouldTransition` is set (a resize-triggered reposition),
16674
- * the visual move is played out via `element.animate()` instead — kept
16675
- * independent of Popover/Dialog/Callout's own opacity/scale/display CSS
16726
+ * Applies a `pickPositionRelativeTo` result to `element`, as a `translate`.
16727
+ *
16728
+ * The two halves of that contract, which a caller must honor:
16729
+ *
16730
+ * 1. `element` stays laid out at its containing block's own origin — its CSS
16731
+ * must say `left: 0; top: 0` (and leave right/bottom `auto`). The computed
16732
+ * `left`/`top` are then applied as a translate from there, which lands the
16733
+ * box in exactly the same place `left: Npx` would have. The reason to go
16734
+ * through a transform at all is the *measuring*: an out-of-flow box with a
16735
+ * shrink-to-fit width can never be wider than "containing block width -
16736
+ * left", so a box placed with `left` reports a width that depends on where
16737
+ * it currently stands — and `pickPositionRelativeTo` decides which side to
16738
+ * place it on from that very width. Feeding a placement back into the
16739
+ * decision that produced it makes the element alternate sides forever, one
16740
+ * flip per reposition. Laid out at the origin, the measured width only ever
16741
+ * depends on the content and on the size caps below.
16742
+ * 2. The `translate` property belongs to this function alone. An animation on
16743
+ * the same element uses `scale` (which composes: individual transform
16744
+ * properties apply translate, then rotate, then scale, then `transform`, so
16745
+ * a scale never rescales the placement) or `transform` — see popup_css.js,
16746
+ * whose slide entrances translate through `transform` for that reason.
16747
+ *
16748
+ * The translate is set instantly (a scroll-triggered reposition should never
16749
+ * lag its target); when `shouldTransition` is set (a resize-triggered
16750
+ * reposition), the visual move is played out via `element.animate()` instead —
16751
+ * kept independent of Popover/Dialog/Callout's own opacity/scale/display CSS
16676
16752
  * transition on the same element, so neither can clobber the other (see
16677
16753
  * notifyPositionTransition's own doc for why a dedicated Animation over a
16678
16754
  * CSS one). Duration comes from `--popup-position-transition-duration`
@@ -16744,18 +16820,19 @@ const applyNewPosition = (
16744
16820
  // A single implicit keyframe turned out not to work here: the WAAPI
16745
16821
  // "neutral" start keyframe isn't frozen at `animate()` call time, it's
16746
16822
  // resolved from the underlying value when the animation is first
16747
- // *sampled* (the next frame) — by then `element.style.left`/`top` below
16823
+ // *sampled* (the next frame) — by then `element.style.translate` below
16748
16824
  // has already been overwritten with the new target, so start === end and
16749
16825
  // nothing visibly moves (observed as the dialog just jumping). Reading
16750
16826
  // the previous value ourselves, before overwriting it, and passing both
16751
16827
  // keyframes explicitly sidesteps that entirely.
16752
- const previousLeft = parseFloat(element.style.left) || left;
16753
- const previousTop = parseFloat(element.style.top) || top;
16828
+ const previousTranslate = parseTranslate(element.style.translate);
16829
+ const previousLeft = previousTranslate ? previousTranslate.x : left;
16830
+ const previousTop = previousTranslate ? previousTranslate.y : top;
16754
16831
  if (shouldTransition) {
16755
16832
  const animation = element.animate(
16756
16833
  [
16757
- { left: `${previousLeft}px`, top: `${previousTop}px` },
16758
- { left: `${left}px`, top: `${top}px` },
16834
+ { translate: `${previousLeft}px ${previousTop}px` },
16835
+ { translate: `${left}px ${top}px` },
16759
16836
  ],
16760
16837
  {
16761
16838
  duration: parseTransitionDurationMs(
@@ -16768,16 +16845,31 @@ const applyNewPosition = (
16768
16845
  );
16769
16846
  notifyPositionTransition(element, animation);
16770
16847
  }
16771
- // The specified `left`/`top` are set to their final target right away,
16848
+ // The specified translate is set to its final target right away,
16772
16849
  // regardless of `shouldTransition` — the animation above only plays the
16773
16850
  // visual move from the old position, it never becomes the actual
16774
16851
  // specified style (see notifyPositionTransition's own commitStyles for
16775
16852
  // why that matters once it ends).
16776
- element.style.left = `${left}px`;
16777
- element.style.top = `${top}px`;
16853
+ element.style.translate = `${left}px ${top}px`;
16778
16854
  dispatchCustomEvent(element, "navi_position_change");
16779
16855
  };
16780
16856
 
16857
+ // "42px 100px" as { x, y }. Anything else — unset, "none", a single-value
16858
+ // shorthand — reads as "never placed yet" (null), so a first placement has no
16859
+ // stale point to be animated from.
16860
+ const parseTranslate = (translate) => {
16861
+ if (!translate) {
16862
+ return null;
16863
+ }
16864
+ const [x, y] = translate.split(" ");
16865
+ const xNumber = parseFloat(x);
16866
+ const yNumber = parseFloat(y);
16867
+ if (Number.isNaN(xNumber) || Number.isNaN(yNumber)) {
16868
+ return null;
16869
+ }
16870
+ return { x: xNumber, y: yNumber };
16871
+ };
16872
+
16781
16873
  const [publishDebugger, subscribeDebugger] = createPubSub();
16782
16874
 
16783
16875
  const notifyDebuggerStart = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.17.31",
3
+ "version": "0.17.33",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {