@jsenv/dom 0.9.5 → 0.10.1

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 +568 -188
  2. package/package.json +2 -2
package/dist/jsenv_dom.js CHANGED
@@ -117,13 +117,16 @@ const getElementSignature = (element) => {
117
117
  return "<html>";
118
118
  }
119
119
  const elementId = element.id;
120
- if (elementId) {
120
+ const className = element.className;
121
+ if (elementId && !looksLikeGeneratedId(elementId)) {
121
122
  return `${tagName}#${elementId}`;
122
123
  }
123
- const className = element.className;
124
124
  if (className) {
125
125
  return `${tagName}.${className.split(" ").join(".")}`;
126
126
  }
127
+ if (elementId) {
128
+ return `${tagName}#${elementId}`;
129
+ }
127
130
 
128
131
  const parentSignature = getElementSignature(element.parentElement);
129
132
  return `${parentSignature} > ${tagName}`;
@@ -131,6 +134,13 @@ const getElementSignature = (element) => {
131
134
  return String(element);
132
135
  };
133
136
 
137
+ // Generated ids from frameworks (Preact useId, React useId, etc.) look like
138
+ // "P0-0", ":r0:", "P1-3" — short alphanumeric tokens with dashes or colons.
139
+ // If an id matches this pattern we prefer className over it.
140
+ const looksLikeGeneratedId = (id) => {
141
+ return /^[A-Z][0-9]+-[0-9]+$|^:[a-z][0-9]*:$/.test(id);
142
+ };
143
+
134
144
  const createIterableWeakSet = () => {
135
145
  const objectWeakRefSet = new Set();
136
146
 
@@ -309,6 +319,7 @@ const elementIsWindow = (a) => a.window === a;
309
319
  const elementIsDocument = (a) => a.nodeType === 9;
310
320
  const elementIsDetails = ({ nodeName }) => nodeName === "DETAILS";
311
321
  const elementIsSummary = ({ nodeName }) => nodeName === "SUMMARY";
322
+ const elementIsDialog = ({ nodeName }) => nodeName === "DIALOG";
312
323
 
313
324
  // should be used ONLY when an element is related to other elements that are not descendants of this element
314
325
  const getAssociatedElements = (element) => {
@@ -3849,6 +3860,16 @@ const getFocusVisibilityInfo = (node) => {
3849
3860
  }
3850
3861
  // Continue checking ancestors
3851
3862
  }
3863
+ if (elementIsDialog(nodeOrAncestor) && !nodeOrAncestor.open) {
3864
+ return { visible: false, reason: "inside closed dialog element" };
3865
+ }
3866
+ if (
3867
+ nodeOrAncestor.popover !== null &&
3868
+ nodeOrAncestor.popover !== undefined &&
3869
+ !nodeOrAncestor.matches(":popover-open")
3870
+ ) {
3871
+ return { visible: false, reason: "inside closed popover element" };
3872
+ }
3852
3873
  nodeOrAncestor = nodeOrAncestor.parentNode;
3853
3874
  }
3854
3875
  return { visible: true, reason: "no reason to be hidden" };
@@ -4661,7 +4682,11 @@ const getNextTablePosition = (
4661
4682
 
4662
4683
  const performTabNavigation = (
4663
4684
  event,
4664
- { rootElement = document.body, outsideOfElement = null } = {},
4685
+ {
4686
+ rootElement = document.body,
4687
+ outsideOfElement = null,
4688
+ debug = () => {},
4689
+ } = {},
4665
4690
  ) => {
4666
4691
  if (!isTabEvent$1(event)) {
4667
4692
  return false;
@@ -4673,29 +4698,20 @@ const performTabNavigation = (
4673
4698
  }
4674
4699
  const isForward = !event.shiftKey;
4675
4700
  const onTargetToFocus = (targetToFocus) => {
4676
- console.debug(
4701
+ debug(
4677
4702
  `Tab navigation: ${isForward ? "forward" : "backward"} from`,
4678
- activeElement,
4703
+ getElementSignature(activeElement),
4679
4704
  "to",
4680
- targetToFocus,
4705
+ getElementSignature(targetToFocus),
4681
4706
  );
4682
4707
  event.preventDefault();
4683
4708
  markFocusNav(event);
4684
4709
  targetToFocus.focus();
4685
4710
  };
4686
4711
 
4687
- {
4688
- console.debug(
4689
- `Tab navigation: ${isForward ? "forward" : "backward"} from,`,
4690
- activeElement,
4691
- );
4692
- }
4693
-
4694
4712
  const predicate = (candidate) => {
4695
4713
  const canBeFocusedByTab = isFocusableByTab(candidate);
4696
- {
4697
- console.debug(`Testing`, candidate, `${canBeFocusedByTab ? "✓" : "✗"}`);
4698
- }
4714
+ // debug(`Testing`, candidate, `${canBeFocusedByTab ? "✓" : "✗"}`);
4699
4715
  return canBeFocusedByTab;
4700
4716
  };
4701
4717
 
@@ -4720,7 +4736,8 @@ const performTabNavigation = (
4720
4736
  if (nextFocusableElement) {
4721
4737
  return onTargetToFocus(nextFocusableElement);
4722
4738
  }
4723
- const firstFocusableElement = findDescendant(activeElement, predicate, {
4739
+ // Wrap around: go back to the first focusable element in root.
4740
+ const firstFocusableElement = findDescendant(rootElement, predicate, {
4724
4741
  skipRoot: outsideOfElement,
4725
4742
  });
4726
4743
  if (firstFocusableElement) {
@@ -4751,7 +4768,8 @@ const performTabNavigation = (
4751
4768
  if (previousFocusableElement) {
4752
4769
  return onTargetToFocus(previousFocusableElement);
4753
4770
  }
4754
- const lastFocusableElement = findLastDescendant(activeElement, predicate, {
4771
+ // Wrap around: go back to the last focusable element in root.
4772
+ const lastFocusableElement = findLastDescendant(rootElement, predicate, {
4755
4773
  skipRoot: outsideOfElement,
4756
4774
  });
4757
4775
  if (lastFocusableElement) {
@@ -4876,7 +4894,35 @@ const preventFocusNavViaKeyboard = (keyboardEvent) => {
4876
4894
  return false;
4877
4895
  };
4878
4896
 
4879
- const trapFocusInside = (element) => {
4897
+ /**
4898
+ * Traps keyboard focus and mouse clicks inside `element`.
4899
+ *
4900
+ * Once active:
4901
+ * - **Tab / Shift+Tab** cycle through focusable descendants of `element`,
4902
+ * wrapping from last → first and first → last. If no focusable element
4903
+ * exists, the default browser Tab action is suppressed so focus cannot
4904
+ * escape.
4905
+ * - **Mouse clicks** outside `element` are only blocked when `pointerTrap`
4906
+ * is `true`. Backdrop clicks (on `<dialog>` elements) still propagate even
4907
+ * then, so the dialog can close itself.
4908
+ *
4909
+ * Multiple traps can be stacked. When a new trap is activated the previous
4910
+ * one is paused; when the new trap is released the previous one resumes.
4911
+ * Traps must be released in LIFO order (the reverse of activation order).
4912
+ *
4913
+ * @param {HTMLElement} element - The root element to trap focus inside.
4914
+ * @param {object} [options]
4915
+ * @param {boolean} [options.pointerTrap=false] - When true, mouse clicks outside `element`
4916
+ * are cancelled so the user cannot move focus away by clicking the backdrop.
4917
+ * Backdrop clicks (target is a `<dialog>` element) only receive `preventDefault`
4918
+ * and still propagate, allowing the dialog to react to them (e.g. close itself).
4919
+ * @param {Function} [options.debug] - Optional debug logger passed to tab navigation.
4920
+ * @returns {() => void} Cleanup function — call it to release the trap.
4921
+ */
4922
+ const trapFocusInside = (
4923
+ element,
4924
+ { debug, pointerTrap = false } = {},
4925
+ ) => {
4880
4926
  if (element.nodeType === 3) {
4881
4927
  console.warn("cannot trap focus inside a text node");
4882
4928
  return () => {};
@@ -4891,39 +4937,67 @@ const trapFocusInside = (element) => {
4891
4937
  }
4892
4938
 
4893
4939
  const isEventOutside = (event) => {
4894
- if (event.target === element) return false;
4895
- if (element.contains(event.target)) return false;
4940
+ if (event.target === element) {
4941
+ return false;
4942
+ }
4943
+ if (element.contains(event.target)) {
4944
+ return false;
4945
+ }
4896
4946
  return true;
4897
4947
  };
4898
4948
 
4899
4949
  const lock = () => {
4900
- const onmousedown = (event) => {
4901
- if (isEventOutside(event)) {
4902
- event.preventDefault();
4903
- event.stopImmediatePropagation();
4904
- }
4905
- };
4950
+ const onmousedown = pointerTrap
4951
+ ? (event) => {
4952
+ if (!isEventOutside(event)) {
4953
+ return;
4954
+ }
4955
+ event.preventDefault();
4956
+ // Backdrop clicks (e.g. clicking a <dialog>'s ::backdrop) must still
4957
+ // propagate so the dialog/popover can react to them (e.g. close itself).
4958
+ // A backdrop click is detected when the target is a <dialog> element —
4959
+ // the ::backdrop pseudo-element is not in the DOM, so the event target
4960
+ // becomes the dialog element itself when its content area is not hit.
4961
+ const isBackdropClick =
4962
+ event.target.tagName === "DIALOG" ||
4963
+ event.target.className.includes("backdrop");
4964
+ if (!isBackdropClick) {
4965
+ event.stopImmediatePropagation();
4966
+ }
4967
+ }
4968
+ : null;
4906
4969
 
4907
4970
  const onkeydown = (event) => {
4908
4971
  if (isTabEvent(event)) {
4909
- performTabNavigation(event, { rootElement: element });
4972
+ const handled = performTabNavigation(event, {
4973
+ rootElement: element,
4974
+ debug,
4975
+ });
4976
+ if (!handled) {
4977
+ // No focusable target found — prevent the browser from moving focus outside the trap.
4978
+ event.preventDefault();
4979
+ }
4910
4980
  }
4911
4981
  };
4912
4982
 
4913
- document.addEventListener("mousedown", onmousedown, {
4914
- capture: true,
4915
- passive: false,
4916
- });
4983
+ if (onmousedown) {
4984
+ document.addEventListener("mousedown", onmousedown, {
4985
+ capture: true,
4986
+ passive: false,
4987
+ });
4988
+ }
4917
4989
  document.addEventListener("keydown", onkeydown, {
4918
4990
  capture: true,
4919
4991
  passive: false,
4920
4992
  });
4921
4993
 
4922
4994
  return () => {
4923
- document.removeEventListener("mousedown", onmousedown, {
4924
- capture: true,
4925
- passive: false,
4926
- });
4995
+ if (onmousedown) {
4996
+ document.removeEventListener("mousedown", onmousedown, {
4997
+ capture: true,
4998
+ passive: false,
4999
+ });
5000
+ }
4927
5001
  document.removeEventListener("keydown", onkeydown, {
4928
5002
  capture: true,
4929
5003
  passive: false,
@@ -5785,11 +5859,228 @@ const getScrollbarState = (
5785
5859
  return { x, y, availableWidth, availableHeight };
5786
5860
  };
5787
5861
 
5862
+ /**
5863
+ * Scrolls el into view within a specific container only — does NOT scroll
5864
+ * any ancestor beyond that container (document, popover backdrop, etc.).
5865
+ *
5866
+ * Why not just use scrollIntoView({ container: "nearest" })?
5867
+ * It finds the nearest scrollable ancestor and stops there ONLY IF that
5868
+ * ancestor has visible scrollbar, otherwise browser walks further up,
5869
+ * potentially scrolling the document.
5870
+ * This is exactly the wrong behavior inside a popover or fixed panel.
5871
+ * scrollIntoViewScoped avoids this by targeting one container explicitly.
5872
+ *
5873
+ * Uses scrollTo() so CSS scroll-behavior:smooth on the container is respected.
5874
+ * Respects scroll-margin-* on the element.
5875
+ *
5876
+ * @param {Element} el - The element to scroll into view.
5877
+ * @param {object} options
5878
+ * @param {Element} [options.container] - The scroll container to scroll. Defaults to getScrollContainer(el).
5879
+ * @param {"start"|"center"|"end"|"nearest"} [options.block="nearest"] - Vertical alignment.
5880
+ * @param {"start"|"center"|"end"|"nearest"} [options.inline="nearest"] - Horizontal alignment.
5881
+ */
5882
+ const scrollIntoViewScoped = (
5883
+ el,
5884
+ {
5885
+ container = getScrollContainer(el),
5886
+ block = "nearest",
5887
+ inline = "nearest",
5888
+ } = {},
5889
+ ) => {
5890
+ if (!container) {
5891
+ return;
5892
+ }
5893
+
5894
+ const containerRect = container.getBoundingClientRect();
5895
+ const elRect = el.getBoundingClientRect();
5896
+ const style = getComputedStyle(el);
5897
+
5898
+ const scrollMarginTop = parseFloat(style.scrollMarginTop) || 0;
5899
+ const scrollMarginBottom = parseFloat(style.scrollMarginBottom) || 0;
5900
+ const scrollMarginLeft = parseFloat(style.scrollMarginLeft) || 0;
5901
+ const scrollMarginRight = parseFloat(style.scrollMarginRight) || 0;
5902
+
5903
+ const currentScrollTop = container.scrollTop;
5904
+ const currentScrollLeft = container.scrollLeft;
5905
+ const containerHeight = containerRect.height;
5906
+ const containerWidth = containerRect.width;
5907
+
5908
+ // Element position relative to the container's scroll origin.
5909
+ const elTop =
5910
+ elRect.top - containerRect.top + currentScrollTop - scrollMarginTop;
5911
+ const elBottom = elTop + elRect.height + scrollMarginTop + scrollMarginBottom;
5912
+ const elLeft =
5913
+ elRect.left - containerRect.left + currentScrollLeft - scrollMarginLeft;
5914
+ const elRight = elLeft + elRect.width + scrollMarginLeft + scrollMarginRight;
5915
+
5916
+ let newScrollTop = currentScrollTop;
5917
+ if (block === "start") {
5918
+ newScrollTop = elTop;
5919
+ } else if (block === "end") {
5920
+ newScrollTop = elBottom - containerHeight;
5921
+ } else if (block === "center") {
5922
+ newScrollTop = elTop + (elRect.height - containerHeight) / 2;
5923
+ } else {
5924
+ // nearest: scroll only if partially or fully out of view.
5925
+ // When the element is taller than the container, only scroll if it is
5926
+ // completely out of view — otherwise it is already as visible as possible.
5927
+ const scrollBottom = currentScrollTop + containerHeight;
5928
+ const elHeight = elBottom - elTop;
5929
+ if (elHeight <= containerHeight) {
5930
+ if (elTop < currentScrollTop) {
5931
+ newScrollTop = elTop;
5932
+ } else if (elBottom > scrollBottom) {
5933
+ newScrollTop = elBottom - containerHeight;
5934
+ }
5935
+ } else if (elBottom < currentScrollTop) {
5936
+ newScrollTop = elBottom - containerHeight;
5937
+ } else if (elTop > scrollBottom) {
5938
+ newScrollTop = elTop;
5939
+ }
5940
+ }
5941
+
5942
+ let newScrollLeft = currentScrollLeft;
5943
+ if (inline === "start") {
5944
+ newScrollLeft = elLeft;
5945
+ } else if (inline === "end") {
5946
+ newScrollLeft = elRight - containerWidth;
5947
+ } else if (inline === "center") {
5948
+ newScrollLeft = elLeft + (elRect.width - containerWidth) / 2;
5949
+ } else {
5950
+ // nearest: scroll only if partially or fully out of view.
5951
+ // When the element is wider than the container, only scroll if it is
5952
+ // completely out of view — otherwise it is already as visible as possible.
5953
+ const scrollRight = currentScrollLeft + containerWidth;
5954
+ const elWidth = elRight - elLeft;
5955
+ if (elWidth <= containerWidth) {
5956
+ if (elLeft < currentScrollLeft) {
5957
+ newScrollLeft = elLeft;
5958
+ } else if (elRight > scrollRight) {
5959
+ newScrollLeft = elRight - containerWidth;
5960
+ }
5961
+ } else if (elRight < currentScrollLeft) {
5962
+ newScrollLeft = elRight - containerWidth;
5963
+ } else if (elLeft > scrollRight) {
5964
+ newScrollLeft = elLeft;
5965
+ }
5966
+ }
5967
+
5968
+ container.scrollTo({
5969
+ left: newScrollLeft,
5970
+ top: newScrollTop,
5971
+ });
5972
+ };
5973
+
5974
+ /**
5975
+ * DON'T USE THIS, use scroll-padding-top/bottom in CSS instead
5976
+ * better in every aspect
5977
+ */
5978
+
5979
+
5980
+ /**
5981
+ * Scrolls el into view (using the native "nearest" block behavior) and then
5982
+ * corrects for any sticky element that visually covers el inside its scroll
5983
+ * container.
5984
+ *
5985
+ * After the native scroll, this function iterates the siblings of el (children
5986
+ * of el's parent) and checks whether any of them uses `position: sticky` and
5987
+ * overlaps el. The largest overlap on each side is used to nudge scrollTop:
5988
+ * - sticky-top (top !== auto): subtract overlap so el appears below the header
5989
+ * - sticky-bottom (bottom !== auto): add overlap so el appears above the footer
5990
+ *
5991
+ * If el happens to be covered on both sides at once (extremely unlikely) the
5992
+ * correction picks whichever side was covered — the result may not be perfect
5993
+ * but avoids an infinite correction loop.
5994
+ *
5995
+ * @param {Element} el - The element to scroll into view.
5996
+ */
5997
+ const scrollIntoViewWithStickyAwareness = (
5998
+ el,
5999
+ { behavior, block = "nearest", inline, container } = {},
6000
+ ) => {
6001
+ el.scrollIntoView({ behavior, block, inline, container });
6002
+ const scrollContainer = getScrollContainer(el);
6003
+ if (!scrollContainer) {
6004
+ return;
6005
+ }
6006
+ const elRect = el.getBoundingClientRect();
6007
+ let topCover = 0;
6008
+ let bottomCover = 0;
6009
+ for (const sibling of el.parentNode.children) {
6010
+ const style = getComputedStyle(sibling);
6011
+ if (style.position !== "sticky") {
6012
+ continue;
6013
+ }
6014
+ const rect = sibling.getBoundingClientRect();
6015
+ if (style.top !== "auto") {
6016
+ // Sticky-top: covers el from above — track the largest overlap.
6017
+ const overlap = rect.bottom - elRect.top;
6018
+ if (overlap > topCover) {
6019
+ topCover = overlap;
6020
+ }
6021
+ } else if (style.bottom !== "auto") {
6022
+ // Sticky-bottom: covers el from below — track the largest overlap.
6023
+ // Only checked when top is "auto" so each element is attributed to one
6024
+ // side only; both sides are still accumulated across all children.
6025
+ const overlap = elRect.bottom - rect.top;
6026
+ if (overlap > bottomCover) {
6027
+ bottomCover = overlap;
6028
+ }
6029
+ }
6030
+ if (topCover > 0 && bottomCover > 0) {
6031
+ // Both sides already have coverage — no point checking further children.
6032
+ break;
6033
+ }
6034
+ }
6035
+ if (topCover > 0) {
6036
+ // For block="center" the element is visually centered in the full viewport.
6037
+ // A sticky header of height H shifts the available center upward by H/2,
6038
+ // so we only need to correct by half the overlap to keep the element
6039
+ // centered in the visible (uncovered) area.
6040
+ scrollContainer.scrollTop -= block === "center" ? topCover / 2 : topCover;
6041
+ }
6042
+ if (bottomCover > 0) {
6043
+ scrollContainer.scrollTop +=
6044
+ block === "center" ? bottomCover / 2 : bottomCover;
6045
+ }
6046
+ };
6047
+
6048
+ /**
6049
+ * Prevents scrolling on all scrollable containers that are ancestors of (or
6050
+ * siblings preceding) `element`. Used when an overlay (popover, dialog) is
6051
+ * open and background scroll should be disabled.
6052
+ *
6053
+ * **Why padding instead of scrollbar-gutter?**
6054
+ * `scrollbar-gutter: stable` would be the modern, CSS-native way to reserve
6055
+ * the scrollbar lane before hiding overflow so the layout doesn't shift.
6056
+ * However it only works well when the element's design already accounts for
6057
+ * that reserved space. On arbitrary containers we can't assume that, so we
6058
+ * measure the actual scrollbar size and compensate with padding — a technique
6059
+ * that works regardless of how the element is styled.
6060
+ *
6061
+ * **What if the element already uses scrollbar-gutter?**
6062
+ * A non-"auto" `scrollbar-gutter` value signals that the element has its own
6063
+ * scrollbar-gutter strategy in place. In that case we skip the padding
6064
+ * compensation and rely on that strategy instead — adding padding on top of an
6065
+ * already-reserved gutter would double-count the space.
6066
+ *
6067
+ * @param {HTMLElement} element - The overlay element being shown. Its preceding
6068
+ * siblings and all ancestor scroll containers will be scroll-locked.
6069
+ * @returns {() => void} Cleanup function that restores all modified styles.
6070
+ */
5788
6071
  const trapScrollInside = (element) => {
5789
6072
  const cleanupCallbackSet = new Set();
5790
6073
  const lockScroll = (el) => {
6074
+ const scrollbarGutter = getStyle(el, "scrollbar-gutter");
6075
+ const hasScrollbarGutterStrategy =
6076
+ scrollbarGutter && scrollbarGutter !== "auto";
6077
+ if (hasScrollbarGutterStrategy) {
6078
+ // The element manages its own gutter — just hide overflow, no padding needed.
6079
+ const removeScrollLockStyles = setStyles(el, { overflow: "hidden" });
6080
+ cleanupCallbackSet.add(removeScrollLockStyles);
6081
+ return;
6082
+ }
5791
6083
  const [scrollbarWidth, scrollbarHeight] = measureScrollbar(el);
5792
- // scrollbar-gutter would work but would display an empty blank space
5793
6084
  const paddingRight = parseInt(getStyle(el, "padding-right"), 0);
5794
6085
  const paddingTop = parseInt(getStyle(el, "padding-top"), 0);
5795
6086
  const removeScrollLockStyles = setStyles(el, {
@@ -5797,9 +6088,7 @@ const trapScrollInside = (element) => {
5797
6088
  "padding-top": `${paddingTop + scrollbarHeight}px`,
5798
6089
  "overflow": "hidden",
5799
6090
  });
5800
- cleanupCallbackSet.add(() => {
5801
- removeScrollLockStyles();
5802
- });
6091
+ cleanupCallbackSet.add(removeScrollLockStyles);
5803
6092
  };
5804
6093
  let previous = element.previousSibling;
5805
6094
  while (previous) {
@@ -9237,24 +9526,27 @@ const stickyAsRelativeCoords = (
9237
9526
  return [leftPosition, topPosition];
9238
9527
  };
9239
9528
 
9240
- // Creates a visible rect effect that tracks how much of an element is visible within its scrollable parent
9241
- // and within the document viewport. This is useful for implementing overlays, lazy loading, or any UI
9242
- // that needs to react to element visibility changes.
9243
- //
9244
- // The function returns two visibility ratios:
9245
- // - scrollVisibilityRatio: Visibility ratio relative to the scrollable parent (0-1)
9246
- // - visibilityRatio: Visibility ratio relative to the document viewport (0-1)
9247
- //
9248
- // When scrollable parent is the document, both ratios will be the same.
9249
- // When scrollable parent is a custom container, scrollVisibilityRatio might be 1.0 (fully visible
9250
- // within the container) while visibilityRatio could be 0.0 (container is scrolled out of viewport).
9251
- // A bit like https://tetherjs.dev/ but different
9529
+ /**
9530
+ * Tracks how much of an element is visible within its scrollable parent and within the
9531
+ * document viewport. Calls update() on initialization and whenever visibility changes
9532
+ * (scroll, resize, intersection changes).
9533
+ *
9534
+ * The update callback receives a visibleRect object with:
9535
+ * - left, top, right, bottom, width, height: the visible portion of the element,
9536
+ * clipped to its scroll container's bounds and expressed in overlay coordinates
9537
+ * - visibilityRatio: fraction of the element's area that is truly visible on screen (0–1).
9538
+ * For document scroll containers this is the viewport-clipped fraction.
9539
+ * For custom containers this is the fraction clipped by both the container AND the viewport
9540
+ * (so an element scrolled out of its container correctly reports 0, not 1).
9541
+ *
9542
+ * A bit like https://tetherjs.dev/ but different
9543
+ */
9252
9544
  const visibleRectEffect = (element, update) => {
9253
9545
  const [teardown, addTeardown] = createPubSub();
9254
9546
  const scrollContainer = getScrollContainer(element);
9255
9547
  const scrollContainerIsDocument =
9256
9548
  scrollContainer === document.documentElement;
9257
- const check = (reason) => {
9549
+ const check = (event) => {
9258
9550
 
9259
9551
  // 1. Calculate element position relative to scrollable parent
9260
9552
  const { scrollLeft, scrollTop } = scrollContainer;
@@ -9346,27 +9638,35 @@ const visibleRectEffect = (element, update) => {
9346
9638
  }
9347
9639
  }
9348
9640
 
9349
- // Calculate visibility ratios
9350
- const scrollVisibilityRatio =
9351
- (widthVisible * heightVisible) / (width * height);
9352
- // Calculate visibility ratio relative to document viewport
9353
- let documentVisibilityRatio;
9641
+ // Calculate visibilityRatio: fraction of element area truly visible on screen.
9642
+ // For custom containers we intersect the container-clipped visible size (widthVisible x
9643
+ // heightVisible) with the viewport bounds, so an element scrolled out of its container
9644
+ // correctly reports 0 rather than the raw viewport intersection of its bounding rect.
9645
+ let visibilityRatio;
9354
9646
  if (scrollContainerIsDocument) {
9355
- documentVisibilityRatio = scrollVisibilityRatio;
9647
+ visibilityRatio = (widthVisible * heightVisible) / (width * height);
9356
9648
  } else {
9357
- // For custom containers, calculate visibility relative to document viewport
9358
- const elementRect = element.getBoundingClientRect();
9649
+ // widthVisible/heightVisible are already clipped to the scroll container.
9650
+ // Now clip their viewport-relative counterparts against the viewport.
9359
9651
  const viewportWidth = window.innerWidth;
9360
9652
  const viewportHeight = window.innerHeight;
9361
- // Calculate how much of the element is visible in the document viewport
9362
- const elementLeft = Math.max(0, elementRect.left);
9363
- const elementTop = Math.max(0, elementRect.top);
9364
- const elementRight = Math.min(viewportWidth, elementRect.right);
9365
- const elementBottom = Math.min(viewportHeight, elementRect.bottom);
9366
- const documentVisibleWidth = Math.max(0, elementRight - elementLeft);
9367
- const documentVisibleHeight = Math.max(0, elementBottom - elementTop);
9368
- documentVisibilityRatio =
9369
- (documentVisibleWidth * documentVisibleHeight) / (width * height);
9653
+ // Container-clipped visible rect in viewport coordinates
9654
+ const visibleLeft = overlayLeft;
9655
+ const visibleTop = overlayTop;
9656
+ const visibleRight = overlayLeft + widthVisible;
9657
+ const visibleBottom = overlayTop + heightVisible;
9658
+ // Intersect with viewport
9659
+ const clippedLeft = visibleLeft < 0 ? 0 : visibleLeft;
9660
+ const clippedTop = visibleTop < 0 ? 0 : visibleTop;
9661
+ const clippedRight =
9662
+ visibleRight > viewportWidth ? viewportWidth : visibleRight;
9663
+ const clippedBottom =
9664
+ visibleBottom > viewportHeight ? viewportHeight : visibleBottom;
9665
+ const clippedWidth =
9666
+ clippedRight > clippedLeft ? clippedRight - clippedLeft : 0;
9667
+ const clippedHeight =
9668
+ clippedBottom > clippedTop ? clippedBottom - clippedTop : 0;
9669
+ visibilityRatio = (clippedWidth * clippedHeight) / (width * height);
9370
9670
  }
9371
9671
 
9372
9672
  const visibleRect = {
@@ -9376,22 +9676,22 @@ const visibleRectEffect = (element, update) => {
9376
9676
  bottom: overlayTop + heightVisible,
9377
9677
  width: widthVisible,
9378
9678
  height: heightVisible,
9379
- visibilityRatio: documentVisibilityRatio,
9380
- scrollVisibilityRatio,
9679
+ visibilityRatio,
9381
9680
  };
9382
9681
  update(visibleRect, {
9682
+ event,
9383
9683
  width,
9384
9684
  height,
9385
9685
  });
9386
9686
  };
9387
9687
 
9388
- check();
9688
+ check(new CustomEvent("initialization"));
9389
9689
 
9390
9690
  const [publishBeforeAutoCheck, onBeforeAutoCheck] = createPubSub();
9391
9691
  {
9392
- const autoCheck = (reason) => {
9393
- const beforeCheckResults = publishBeforeAutoCheck(reason);
9394
- check();
9692
+ const autoCheck = (event) => {
9693
+ const beforeCheckResults = publishBeforeAutoCheck(event);
9694
+ check(event);
9395
9695
  for (const beforeCheckResult of beforeCheckResults) {
9396
9696
  if (typeof beforeCheckResult === "function") {
9397
9697
  beforeCheckResult();
@@ -9412,8 +9712,8 @@ const visibleRectEffect = (element, update) => {
9412
9712
  {
9413
9713
  // If scrollable parent is not document, also listen to document scroll
9414
9714
  // to update UI position when the scrollable parent moves in viewport
9415
- const onDocumentScroll = () => {
9416
- autoCheck("document_scroll");
9715
+ const onDocumentScroll = (e) => {
9716
+ autoCheck(e);
9417
9717
  };
9418
9718
  document.addEventListener("scroll", onDocumentScroll, {
9419
9719
  passive: true,
@@ -9424,8 +9724,8 @@ const visibleRectEffect = (element, update) => {
9424
9724
  });
9425
9725
  });
9426
9726
  if (!scrollContainerIsDocument) {
9427
- const onScroll = () => {
9428
- autoCheck("scrollable_parent_scroll");
9727
+ const onScroll = (e) => {
9728
+ autoCheck(e);
9429
9729
  };
9430
9730
  scrollContainer.addEventListener("scroll", onScroll, {
9431
9731
  passive: true,
@@ -9438,8 +9738,8 @@ const visibleRectEffect = (element, update) => {
9438
9738
  }
9439
9739
  }
9440
9740
  {
9441
- const onWindowResize = () => {
9442
- autoCheck("window_size_change");
9741
+ const onWindowResize = (e) => {
9742
+ autoCheck(e);
9443
9743
  };
9444
9744
  window.addEventListener("resize", onWindowResize);
9445
9745
  addTeardown(() => {
@@ -9469,7 +9769,9 @@ const visibleRectEffect = (element, update) => {
9469
9769
  {
9470
9770
  const documentIntersectionObserver = new IntersectionObserver(
9471
9771
  () => {
9472
- autoCheck("element_intersection_with_document_change");
9772
+ autoCheck(
9773
+ new CustomEvent("element_intersection_with_document_change"),
9774
+ );
9473
9775
  },
9474
9776
  {
9475
9777
  root: null,
@@ -9484,7 +9786,9 @@ const visibleRectEffect = (element, update) => {
9484
9786
  if (!scrollContainerIsDocument) {
9485
9787
  const scrollIntersectionObserver = new IntersectionObserver(
9486
9788
  () => {
9487
- autoCheck("element_intersection_with_scroll_change");
9789
+ autoCheck(
9790
+ new CustomEvent("element_intersection_with_scroll_change"),
9791
+ );
9488
9792
  },
9489
9793
  {
9490
9794
  root: scrollContainer,
@@ -9499,8 +9803,8 @@ const visibleRectEffect = (element, update) => {
9499
9803
  }
9500
9804
  }
9501
9805
  {
9502
- const onWindowTouchMove = () => {
9503
- autoCheck("window_touchmove");
9806
+ const onWindowTouchMove = (e) => {
9807
+ autoCheck(e);
9504
9808
  };
9505
9809
  window.addEventListener("touchmove", onWindowTouchMove, {
9506
9810
  passive: true,
@@ -9522,14 +9826,50 @@ const visibleRectEffect = (element, update) => {
9522
9826
  };
9523
9827
  };
9524
9828
 
9829
+ /**
9830
+ * Places element adjacent to anchor using one of 9 compass positions.
9831
+ *
9832
+ * ```
9833
+ * top-left | top | top-right
9834
+ * ----------+---------+----------
9835
+ * left | center | right
9836
+ * ----------+---------+----------
9837
+ * bottom-left| bottom |bottom-right
9838
+ * ```
9839
+ *
9840
+ * All positions except "center" place element outside the anchor:
9841
+ * - "top" → element.bottom = anchor.top, horizontally centered
9842
+ * - "bottom" → element.top = anchor.bottom, horizontally centered (default)
9843
+ * - "left" → element.right = anchor.left, vertically centered
9844
+ * - "right" → element.left = anchor.right, vertically centered
9845
+ * - "top-left" → element.bottom = anchor.top, element.right = anchor.left
9846
+ * - "top-right" → element.bottom = anchor.top, element.left = anchor.right
9847
+ * - "bottom-left" → element.top = anchor.bottom, element.right = anchor.left
9848
+ * - "bottom-right" → element.top = anchor.bottom, element.left = anchor.right
9849
+ * - "center" → element centered on anchor (overlapping)
9850
+ *
9851
+ * @param {HTMLElement} element - The element to position (must be document-relative)
9852
+ * @param {HTMLElement} anchor - The anchor element to position against
9853
+ * @param {object} [options]
9854
+ * @param {string} [options.positionTry="bottom"] - Preferred position. Mimics CSS position-try.
9855
+ * If it does not fit, the logical opposite is tried automatically:
9856
+ * top↔bottom, left↔right, top-left↔bottom-right, top-right↔bottom-left.
9857
+ * The element's data-position-try attribute takes precedence over this param;
9858
+ * the last resolved position is persisted as data-position-current to avoid flickering.
9859
+ * @param {string} [options.position] - Force a specific position, skipping the fit-check.
9860
+ * @param {number} [options.alignToViewportEdgeWhenAnchorNearEdge=0] - Snap to viewport left
9861
+ * edge when anchor is within this many px of the left edge and element is wider than anchor.
9862
+ * @param {number} [options.minLeft=0] - Minimum left coordinate (document-relative).
9863
+ * @returns {{ position, left, top, width, height, anchorLeft, anchorTop, anchorRight, anchorBottom, spaceAbove, spaceBelow }}
9864
+ */
9525
9865
  const pickPositionRelativeTo = (
9526
9866
  element,
9527
- target,
9867
+ anchor,
9528
9868
  {
9529
- alignToViewportEdgeWhenTargetNearEdge = 0,
9869
+ positionTry = "bottom",
9870
+ position,
9871
+ alignToViewportEdgeWhenAnchorNearEdge = 0,
9530
9872
  minLeft = 0,
9531
- positionPreference,
9532
- forcePosition,
9533
9873
  } = {},
9534
9874
  ) => {
9535
9875
 
@@ -9537,7 +9877,7 @@ const pickPositionRelativeTo = (
9537
9877
  const viewportHeight = document.documentElement.clientHeight;
9538
9878
  // Get viewport-relative positions
9539
9879
  const elementRect = element.getBoundingClientRect();
9540
- const targetRect = target.getBoundingClientRect();
9880
+ const anchorRect = anchor.getBoundingClientRect();
9541
9881
  const {
9542
9882
  left: elementLeft,
9543
9883
  right: elementRight,
@@ -9545,50 +9885,97 @@ const pickPositionRelativeTo = (
9545
9885
  bottom: elementBottom,
9546
9886
  } = elementRect;
9547
9887
  const {
9548
- left: targetLeft,
9549
- right: targetRight,
9550
- top: targetTop,
9551
- bottom: targetBottom,
9552
- } = targetRect;
9888
+ left: anchorLeft,
9889
+ right: anchorRight,
9890
+ top: anchorTop,
9891
+ bottom: anchorBottom,
9892
+ } = anchorRect;
9553
9893
  const elementWidth = elementRight - elementLeft;
9554
9894
  const elementHeight = elementBottom - elementTop;
9555
- const targetWidth = targetRight - targetLeft;
9895
+ const anchorWidth = anchorRight - anchorLeft;
9896
+ const anchorHeight = anchorBottom - anchorTop;
9897
+
9898
+ // Determine the active position: position wins, then data-position-current (last resolved),
9899
+ // then data-position-try attribute (user preference), then positionTry param
9900
+ let activePosition;
9901
+ if (position) {
9902
+ activePosition = position;
9903
+ } else {
9904
+ const positionCurrentFromAttribute = element.getAttribute(
9905
+ "data-position-current",
9906
+ );
9907
+ const positionTryFromAttribute = element.getAttribute("data-position-try");
9908
+ activePosition =
9909
+ positionCurrentFromAttribute || positionTryFromAttribute || positionTry;
9910
+ }
9911
+
9912
+ const spaceAbove = anchorTop;
9913
+ const spaceBelow = viewportHeight - anchorBottom;
9914
+
9915
+ // Resolve vertical axis, falling back to opposite if the tried position does not fit
9916
+ const { isTop, isBottom, isLeft, isRight, isCenter } =
9917
+ decomposePosition(activePosition);
9918
+ const isCenterX = !isLeft && !isRight; // top / bottom / center
9919
+ const isCenterY = !isTop && !isBottom; // left / right / center
9920
+
9921
+ let resolvedVertical; // "top" | "bottom" | "center-y"
9922
+ if (isCenter || isCenterY) {
9923
+ resolvedVertical = "center-y";
9924
+ } else if (position) {
9925
+ resolvedVertical = isTop ? "top" : "bottom";
9926
+ } else if (isTop) {
9927
+ const minContentVisibilityRatio = 0.6;
9928
+ const fitsAbove = spaceAbove / elementHeight >= minContentVisibilityRatio;
9929
+ if (fitsAbove) {
9930
+ resolvedVertical = "top";
9931
+ } else {
9932
+ resolvedVertical = "bottom"; // opposite of top
9933
+ }
9934
+ } else {
9935
+ // isBottom
9936
+ const elementFitsBelow = spaceBelow >= elementHeight;
9937
+ if (elementFitsBelow) {
9938
+ resolvedVertical = "bottom";
9939
+ } else {
9940
+ resolvedVertical = "top"; // opposite of bottom
9941
+ }
9942
+ }
9556
9943
 
9557
9944
  // Calculate horizontal position (viewport-relative)
9558
9945
  let elementPositionLeft;
9559
9946
  {
9560
- // Check if target element is wider than viewport
9561
- const targetIsWiderThanViewport = targetWidth > viewportWidth;
9562
- if (targetIsWiderThanViewport) {
9563
- const targetLeftIsVisible = targetLeft >= 0;
9564
- const targetRightIsVisible = targetRight <= viewportWidth;
9565
-
9566
- if (!targetLeftIsVisible && targetRightIsVisible) {
9567
- // Target extends beyond left edge but right side is visible
9568
- const viewportCenter = viewportWidth / 2;
9569
- const distanceFromRightEdge = viewportWidth - targetRight;
9570
- elementPositionLeft =
9571
- viewportCenter - distanceFromRightEdge / 2 - elementWidth / 2;
9572
- } else if (targetLeftIsVisible && !targetRightIsVisible) {
9573
- // Target extends beyond right edge but left side is visible
9574
- const viewportCenter = viewportWidth / 2;
9575
- const distanceFromLeftEdge = -targetLeft;
9576
- elementPositionLeft =
9577
- viewportCenter - distanceFromLeftEdge / 2 - elementWidth / 2;
9578
- } else {
9579
- // Target extends beyond both edges or is fully visible (center in viewport)
9580
- elementPositionLeft = viewportWidth / 2 - elementWidth / 2;
9581
- }
9947
+ if (isLeft) {
9948
+ elementPositionLeft = anchorLeft - elementWidth;
9949
+ } else if (isRight) {
9950
+ elementPositionLeft = anchorRight;
9582
9951
  } else {
9583
- // Target fits within viewport width - center element relative to target
9584
- elementPositionLeft = targetLeft + targetWidth / 2 - elementWidth / 2;
9585
- // Special handling when element is wider than target
9586
- if (alignToViewportEdgeWhenTargetNearEdge) {
9587
- const elementIsWiderThanTarget = elementWidth > targetWidth;
9588
- const targetIsNearLeftEdge =
9589
- targetLeft < alignToViewportEdgeWhenTargetNearEdge;
9590
- if (elementIsWiderThanTarget && targetIsNearLeftEdge) {
9591
- elementPositionLeft = minLeft; // Left edge of viewport
9952
+ // centered horizontally on anchor
9953
+ const anchorIsWiderThanViewport = anchorWidth > viewportWidth;
9954
+ if (anchorIsWiderThanViewport) {
9955
+ const anchorLeftIsVisible = anchorLeft >= 0;
9956
+ const anchorRightIsVisible = anchorRight <= viewportWidth;
9957
+ if (!anchorLeftIsVisible && anchorRightIsVisible) {
9958
+ const viewportCenter = viewportWidth / 2;
9959
+ const distanceFromRightEdge = viewportWidth - anchorRight;
9960
+ elementPositionLeft =
9961
+ viewportCenter - distanceFromRightEdge / 2 - elementWidth / 2;
9962
+ } else if (anchorLeftIsVisible && !anchorRightIsVisible) {
9963
+ const viewportCenter = viewportWidth / 2;
9964
+ const distanceFromLeftEdge = -anchorLeft;
9965
+ elementPositionLeft =
9966
+ viewportCenter - distanceFromLeftEdge / 2 - elementWidth / 2;
9967
+ } else {
9968
+ elementPositionLeft = viewportWidth / 2 - elementWidth / 2;
9969
+ }
9970
+ } else {
9971
+ elementPositionLeft = anchorLeft + anchorWidth / 2 - elementWidth / 2;
9972
+ if (alignToViewportEdgeWhenAnchorNearEdge) {
9973
+ const elementIsWiderThanAnchor = elementWidth > anchorWidth;
9974
+ const anchorIsNearLeftEdge =
9975
+ anchorLeft < alignToViewportEdgeWhenAnchorNearEdge;
9976
+ if (elementIsWiderThanAnchor && anchorIsNearLeftEdge) {
9977
+ elementPositionLeft = minLeft;
9978
+ }
9592
9979
  }
9593
9980
  }
9594
9981
  }
@@ -9601,83 +9988,76 @@ const pickPositionRelativeTo = (
9601
9988
  }
9602
9989
 
9603
9990
  // Calculate vertical position (viewport-relative)
9604
- let position;
9605
- const spaceAboveTarget = targetTop;
9606
- const spaceBelowTarget = viewportHeight - targetBottom;
9607
- determine_position: {
9608
- if (forcePosition) {
9609
- position = forcePosition;
9610
- break determine_position;
9611
- }
9612
- const elementPreferredPosition = element.getAttribute("data-position");
9613
- const minContentVisibilityRatio = 0.6; // 60% minimum visibility to keep position
9614
-
9615
- // Check positionPreference parameter first, then element attribute
9616
- const preferredPosition = positionPreference || elementPreferredPosition;
9617
-
9618
- if (preferredPosition) {
9619
- // Element has a preferred position - try to keep it unless we really struggle
9620
- const visibleRatio =
9621
- preferredPosition === "above"
9622
- ? spaceAboveTarget / elementHeight
9623
- : spaceBelowTarget / elementHeight;
9624
- const canShowMinimumContent = visibleRatio >= minContentVisibilityRatio;
9625
- if (canShowMinimumContent) {
9626
- position = preferredPosition;
9627
- break determine_position;
9628
- }
9629
- }
9630
- // No preferred position - use original logic (prefer below, fallback to above if more space)
9631
- const elementFitsBelow = spaceBelowTarget >= elementHeight;
9632
- if (elementFitsBelow) {
9633
- position = "below";
9634
- break determine_position;
9991
+ let elementPositionTop;
9992
+ {
9993
+ if (resolvedVertical === "center-y") {
9994
+ elementPositionTop = anchorTop + anchorHeight / 2 - elementHeight / 2;
9995
+ } else if (resolvedVertical === "bottom") {
9996
+ const idealTop = anchorBottom;
9997
+ elementPositionTop =
9998
+ idealTop % 1 === 0 ? idealTop : Math.floor(idealTop) + 1;
9999
+ } else {
10000
+ // "top"
10001
+ const idealTop = anchorTop - elementHeight;
10002
+ elementPositionTop = idealTop < 0 ? 0 : idealTop;
9635
10003
  }
9636
- const hasMoreSpaceBelow = spaceBelowTarget >= spaceAboveTarget;
9637
- position = hasMoreSpaceBelow ? "below" : "above";
9638
10004
  }
9639
10005
 
9640
- let elementPositionTop;
10006
+ let finalPosition;
9641
10007
  {
9642
- if (position === "below") {
9643
- // Calculate top position when placing below target (ensure whole pixels)
9644
- const idealTopWhenBelow = targetBottom;
9645
- elementPositionTop =
9646
- idealTopWhenBelow % 1 === 0
9647
- ? idealTopWhenBelow
9648
- : Math.floor(idealTopWhenBelow) + 1;
10008
+ const vertPart = resolvedVertical === "center-y" ? "" : resolvedVertical;
10009
+ const horzPart = isCenterX ? "" : isLeft ? "left" : "right";
10010
+ if (vertPart && horzPart) {
10011
+ finalPosition = `${vertPart}-${horzPart}`;
10012
+ } else if (vertPart) {
10013
+ finalPosition = vertPart;
10014
+ } else if (horzPart) {
10015
+ finalPosition = horzPart;
9649
10016
  } else {
9650
- // Calculate top position when placing above target
9651
- const idealTopWhenAbove = targetTop - elementHeight;
9652
- const minimumTopInViewport = 0;
9653
- elementPositionTop =
9654
- idealTopWhenAbove < minimumTopInViewport
9655
- ? minimumTopInViewport
9656
- : idealTopWhenAbove;
10017
+ finalPosition = "center";
9657
10018
  }
9658
10019
  }
9659
10020
 
10021
+ // Persist the resolved position on the element so subsequent calls start from it
10022
+ // (avoids flickering between positions when the element is near the threshold).
10023
+ // position is not persisted — it is always explicit.
10024
+
10025
+ if (!position) {
10026
+ element.setAttribute("data-position-current", finalPosition);
10027
+ }
10028
+
9660
10029
  // Get document scroll for final coordinate conversion
9661
10030
  const { scrollLeft, scrollTop } = document.documentElement;
9662
10031
  const elementDocumentLeft = elementPositionLeft + scrollLeft;
9663
10032
  const elementDocumentTop = elementPositionTop + scrollTop;
9664
- const targetDocumentLeft = targetLeft + scrollLeft;
9665
- const targetDocumentTop = targetTop + scrollTop;
9666
- const targetDocumentRight = targetRight + scrollLeft;
9667
- const targetDocumentBottom = targetBottom + scrollTop;
10033
+ const anchorDocumentLeft = anchorLeft + scrollLeft;
10034
+ const anchorDocumentTop = anchorTop + scrollTop;
10035
+ const anchorDocumentRight = anchorRight + scrollLeft;
10036
+ const anchorDocumentBottom = anchorBottom + scrollTop;
9668
10037
 
9669
10038
  return {
9670
- position,
10039
+ position: finalPosition,
9671
10040
  left: elementDocumentLeft,
9672
10041
  top: elementDocumentTop,
9673
10042
  width: elementWidth,
9674
10043
  height: elementHeight,
9675
- targetLeft: targetDocumentLeft,
9676
- targetTop: targetDocumentTop,
9677
- targetRight: targetDocumentRight,
9678
- targetBottom: targetDocumentBottom,
9679
- spaceAboveTarget,
9680
- spaceBelowTarget,
10044
+ anchorLeft: anchorDocumentLeft,
10045
+ anchorTop: anchorDocumentTop,
10046
+ anchorRight: anchorDocumentRight,
10047
+ anchorBottom: anchorDocumentBottom,
10048
+ spaceAbove,
10049
+ spaceBelow,
10050
+ };
10051
+ };
10052
+ // Decompose position flags
10053
+ const decomposePosition = (pos) => {
10054
+ return {
10055
+ isTop: pos === "top" || pos === "top-left" || pos === "top-right",
10056
+ isBottom:
10057
+ pos === "bottom" || pos === "bottom-left" || pos === "bottom-right",
10058
+ isLeft: pos === "left" || pos === "top-left" || pos === "bottom-left",
10059
+ isRight: pos === "right" || pos === "top-right" || pos === "bottom-right",
10060
+ isCenter: pos === "center",
9681
10061
  };
9682
10062
  };
9683
10063
 
@@ -12804,4 +13184,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
12804
13184
  };
12805
13185
  };
12806
13186
 
12807
- export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isSameColor, isScrollable, measureScrollbar, mergeOneStyle, mergeTwoStyles, normalizeStyles, parseStyle, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
13187
+ export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isSameColor, isScrollable, measureScrollbar, mergeOneStyle, mergeTwoStyles, normalizeStyles, parseStyle, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.9.5",
3
+ "version": "0.10.1",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {
@@ -40,7 +40,7 @@
40
40
  "@jsenv/navi": "../navi",
41
41
  "@jsenv/snapshot": "../../tooling/snapshot",
42
42
  "@preact/signals": "2.9.0",
43
- "preact": "11.0.0-beta.0"
43
+ "preact": "11.0.0-beta.1"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"