@jsenv/dom 0.14.6 → 0.15.0

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 +867 -187
  2. package/package.json +2 -2
package/dist/jsenv_dom.js CHANGED
@@ -5514,6 +5514,16 @@ const performTabNavigation = (
5514
5514
  outsideOfElement = null,
5515
5515
  debug = () => {},
5516
5516
  excludeAriaHidden,
5517
+ // When reaching the edge of rootElement would normally wrap back
5518
+ // around inside it, escapeRoot changes that: Tab instead continues
5519
+ // past escapeRoot's *entire* subtree (not just rootElement's), landing
5520
+ // on the next/previous focusable element in the document beyond it.
5521
+ // Used by focus_trap.js when boundaryElement is a real container
5522
+ // (not document) — a trapped element nested inside a bigger container
5523
+ // (e.g. a local-layer Dialog) shouldn't just wrap on itself; Tab should
5524
+ // exit the whole container, skipping over any other focusable
5525
+ // siblings inside it (they're not part of what's actually trapped).
5526
+ escapeRoot = null,
5517
5527
  } = {},
5518
5528
  ) => {
5519
5529
  if (!isTabEvent$1(event)) {
@@ -5626,6 +5636,18 @@ const performTabNavigation = (
5626
5636
  if (nextFocusableElement) {
5627
5637
  return onTargetToFocus(nextFocusableElement);
5628
5638
  }
5639
+ if (escapeRoot) {
5640
+ // Skip escapeRoot's own children entirely — anything else still
5641
+ // inside it (a sibling of rootElement) isn't part of what's
5642
+ // trapped, so it must never become the next Tab stop either.
5643
+ const nextOutsideEscapeRoot = findAfter(escapeRoot, predicate, {
5644
+ skipChildren: true,
5645
+ });
5646
+ if (nextOutsideEscapeRoot) {
5647
+ return onTargetToFocus(nextOutsideEscapeRoot);
5648
+ }
5649
+ return false;
5650
+ }
5629
5651
  // Wrap around: go back to the first focusable element in root.
5630
5652
  const firstFocusableElement = findDescendant(rootElement, predicate, {
5631
5653
  skipRoot: outsideOfElement,
@@ -5658,6 +5680,16 @@ const performTabNavigation = (
5658
5680
  if (previousFocusableElement) {
5659
5681
  return onTargetToFocus(previousFocusableElement);
5660
5682
  }
5683
+ if (escapeRoot) {
5684
+ // findBefore already searches strictly *before* escapeRoot's own
5685
+ // position (previous sibling / ancestor's previous sibling), never
5686
+ // descending into its children — exactly "outside its subtree".
5687
+ const previousOutsideEscapeRoot = findBefore(escapeRoot, predicate);
5688
+ if (previousOutsideEscapeRoot) {
5689
+ return onTargetToFocus(previousOutsideEscapeRoot);
5690
+ }
5691
+ return false;
5692
+ }
5661
5693
  // Wrap around: go back to the last focusable element in root.
5662
5694
  const lastFocusableElement = findLastDescendant(rootElement, predicate, {
5663
5695
  skipRoot: outsideOfElement,
@@ -5855,12 +5887,19 @@ const preventFocusNavViaKeyboard = (keyboardEvent) => {
5855
5887
  *
5856
5888
  * Once active:
5857
5889
  * - **Tab / Shift+Tab** cycle through focusable descendants of `element`,
5858
- * wrapping from last → first and first → last. If no focusable element
5859
- * exists, the default browser Tab action is suppressed so focus cannot
5860
- * escape.
5890
+ * wrapping from last → first and first → last *unless* `boundaryElement`
5891
+ * is a real container (not `document`), in which case Tab escapes the
5892
+ * whole container instead of wrapping (see `boundaryElement`'s own doc).
5893
+ * If no focusable element exists, the default browser Tab action is
5894
+ * suppressed so focus cannot escape.
5861
5895
  * - **Mouse clicks** outside `element` are only blocked when `pointerTrap`
5862
5896
  * is `true`. Backdrop clicks (on `<dialog>` elements) still propagate even
5863
5897
  * then, so the dialog can close itself.
5898
+ * - **Focus entering `boundaryElement` from outside it** (e.g. a `focus()`
5899
+ * call, or Tab arriving from further out in the document) always lands on
5900
+ * `element`'s own first focusable descendant — never on some other
5901
+ * focusable sibling `boundaryElement` happens to also contain. Only
5902
+ * relevant when `boundaryElement` isn't `document` (see below).
5864
5903
  *
5865
5904
  * Multiple traps can be stacked. When a new trap is activated the previous
5866
5905
  * one is paused; when the new trap is released the previous one resumes.
@@ -5873,11 +5912,30 @@ const preventFocusNavViaKeyboard = (keyboardEvent) => {
5873
5912
  * Backdrop clicks (target is a `<dialog>` element) only receive `preventDefault`
5874
5913
  * and still propagate, allowing the dialog to react to them (e.g. close itself).
5875
5914
  * @param {Function} [options.debug] - Optional debug logger passed to tab navigation.
5915
+ * @param {Document|HTMLElement} [options.boundaryElement=document] - Where the
5916
+ * mousedown/keydown/focusin listeners are attached. Defaults to `document`
5917
+ * (a genuinely page-wide modal — the usual case, where none of the
5918
+ * container-specific behavior below applies). Pass a specific container
5919
+ * element instead for a trap that should only apply *within* that
5920
+ * container: a Tab press or click occurring entirely outside it never
5921
+ * reaches a listener attached there at all (events only bubble through
5922
+ * their own ancestor chain), so the rest of the page keeps its normal tab
5923
+ * order/interactions untouched. Inside the container, `element` behaves
5924
+ * as if it were the *only* focusable thing `boundaryElement` contains:
5925
+ * Tab reaching either edge of `element` skips over any other focusable
5926
+ * sibling sharing the container, exiting the container entirely (not
5927
+ * wrapping back into `element`), and focus arriving at some other
5928
+ * focusable sibling inside the container gets redirected into `element`'s
5929
+ * own first focusable descendant instead. Used by Dialog's own
5930
+ * `layer="local"` renderer, which is only meant to be modal within its
5931
+ * own positioned ancestor, not the whole document — a case where that
5932
+ * ancestor can genuinely contain other, unrelated focusable content
5933
+ * (e.g. a trigger button placed right next to it).
5876
5934
  * @returns {() => void} Cleanup function — call it to release the trap.
5877
5935
  */
5878
5936
  const trapFocusInside = (
5879
5937
  element,
5880
- { debug, pointerTrap = false } = {},
5938
+ { debug, pointerTrap = false, boundaryElement = document } = {},
5881
5939
  ) => {
5882
5940
  if (element.nodeType === 3) {
5883
5941
  console.warn("cannot trap focus inside a text node");
@@ -5902,6 +5960,10 @@ const trapFocusInside = (
5902
5960
  return true;
5903
5961
  };
5904
5962
 
5963
+ // A real container (not document) — element must behave as the only
5964
+ // focusable thing boundaryElement contains, see this file's own doc.
5965
+ const escapeRoot = boundaryElement === document ? null : boundaryElement;
5966
+
5905
5967
  const lock = () => {
5906
5968
  const onmousedown = pointerTrap
5907
5969
  ? (event) => {
@@ -5928,6 +5990,7 @@ const trapFocusInside = (
5928
5990
  const handled = performTabNavigation(event, {
5929
5991
  rootElement: element,
5930
5992
  debug,
5993
+ escapeRoot,
5931
5994
  });
5932
5995
  if (!handled) {
5933
5996
  // No focusable target found — prevent the browser from moving focus outside the trap.
@@ -5936,25 +5999,50 @@ const trapFocusInside = (
5936
5999
  }
5937
6000
  };
5938
6001
 
6002
+ // Focus landing on some other focusable sibling boundaryElement also
6003
+ // contains (not element itself) gets redirected into element's own
6004
+ // first focusable descendant — e.g. a direct .focus() call, or Tab
6005
+ // arriving from further out in the document. Click-driven focus theft
6006
+ // is already prevented above by onmousedown (when pointerTrap is on);
6007
+ // this covers the rest (keyboard-driven entry, programmatic focus()).
6008
+ const onfocusin = escapeRoot
6009
+ ? (event) => {
6010
+ const target = event.target;
6011
+ if (target === element || element.contains(target)) {
6012
+ return;
6013
+ }
6014
+ const firstFocusable = findDescendant(element, (node) =>
6015
+ elementIsFocusable(node),
6016
+ );
6017
+ firstFocusable?.focus();
6018
+ }
6019
+ : null;
6020
+
5939
6021
  if (onmousedown) {
5940
- document.addEventListener("mousedown", onmousedown, {
6022
+ boundaryElement.addEventListener("mousedown", onmousedown, {
5941
6023
  capture: true,
5942
6024
  passive: false,
5943
6025
  });
5944
6026
  }
5945
- document.addEventListener("keydown", onkeydown, {
6027
+ boundaryElement.addEventListener("keydown", onkeydown, {
5946
6028
  capture: true,
5947
6029
  passive: false,
5948
6030
  });
6031
+ if (onfocusin) {
6032
+ boundaryElement.addEventListener("focusin", onfocusin);
6033
+ }
5949
6034
 
5950
6035
  return () => {
5951
6036
  if (onmousedown) {
5952
- document.removeEventListener("mousedown", onmousedown, {
6037
+ boundaryElement.removeEventListener("mousedown", onmousedown, {
5953
6038
  capture: true,
5954
6039
  passive: false,
5955
6040
  });
5956
6041
  }
5957
- document.removeEventListener("keydown", onkeydown, {
6042
+ if (onfocusin) {
6043
+ boundaryElement.removeEventListener("focusin", onfocusin);
6044
+ }
6045
+ boundaryElement.removeEventListener("keydown", onkeydown, {
5958
6046
  capture: true,
5959
6047
  passive: false,
5960
6048
  });
@@ -6640,6 +6728,24 @@ const viewportPosToScrollRelativePos = (
6640
6728
  ];
6641
6729
  };
6642
6730
 
6731
+ // position: fixed is already viewport-relative, so no scroll offset is
6732
+ // needed to place it correctly — adding one would double-count the scroll.
6733
+ // position: absolute (assumed relative to the initial containing block, the
6734
+ // common case for a document-relative absolutely positioned element) needs
6735
+ // the current scroll offset added to convert a viewport-relative coordinate
6736
+ // into one it can be set to directly. Read the element's own computed style
6737
+ // rather than assuming one or the other, since callers may use either.
6738
+ const getPositioningScrollOffset = (element) => {
6739
+ const isFixed = getComputedStyle(element).position === "fixed";
6740
+ if (isFixed) {
6741
+ return { scrollLeft: 0, scrollTop: 0 };
6742
+ }
6743
+ return {
6744
+ scrollLeft: documentElement$1.scrollLeft,
6745
+ scrollTop: documentElement$1.scrollTop,
6746
+ };
6747
+ };
6748
+
6643
6749
  const addScrollToRect = (scrollRelativeRect) => {
6644
6750
  const { left, top, width, height, scrollLeft, scrollTop } =
6645
6751
  scrollRelativeRect;
@@ -10774,6 +10880,28 @@ const getPositionedParent = (element) => {
10774
10880
  return document.body;
10775
10881
  };
10776
10882
 
10883
+ /**
10884
+ * Like `getPositionedParent`, but aware of `element` itself being promoted
10885
+ * to the top layer: an element with a `popover` attribute, or a `<dialog>`,
10886
+ * always uses the initial containing block (the viewport) once shown,
10887
+ * regardless of `position` or DOM ancestry — walking up its own parent
10888
+ * chain looking for a positioned ancestor (what `getPositionedParent` does)
10889
+ * would give the wrong answer for these two specifically, since their real
10890
+ * DOM position becomes irrelevant to their own containing block the moment
10891
+ * they're actually open.
10892
+ *
10893
+ * Returns `null` to mean "the viewport" (matching how a real anchor
10894
+ * resolves to `null`/no-anchor callers already treat that as a request for
10895
+ * viewport-relative positioning) for a popover/dialog element;
10896
+ * `getPositionedParent(element)` otherwise.
10897
+ */
10898
+ const getPositioningContainer = (element) => {
10899
+ if (element.hasAttribute("popover") || element.tagName === "DIALOG") {
10900
+ return null;
10901
+ }
10902
+ return getPositionedParent(element);
10903
+ };
10904
+
10777
10905
  const getHeight = (element) => {
10778
10906
  const { height } = element.getBoundingClientRect();
10779
10907
  return height;
@@ -11100,6 +11228,49 @@ const stickyAsRelativeCoords = (
11100
11228
  return [leftPosition, topPosition];
11101
11229
  };
11102
11230
 
11231
+ // Both "resize" sources fire transiently on mobile (keyboard/UI chrome
11232
+ // briefly shifting when focus moves between inputs) — debounced so
11233
+ // consumers skip that in-between state. One shared timer per source (not
11234
+ // one per subscriber) so everything settles on the same tick.
11235
+ const RESIZE_SETTLE_MS = 100;
11236
+
11237
+ // Set while a visualViewport resize is debouncing, cleared once it settles —
11238
+ // read by the window resize listener below.
11239
+ let visualViewportResizePending = false;
11240
+
11241
+ const [publishVisualViewportResize, subscribeVisualViewportResizeSettled] =
11242
+ createPubSub();
11243
+ const [publishWindowResize, subscribeWindowResizeSettled] = createPubSub();
11244
+
11245
+ if (window.visualViewport) {
11246
+ let timeoutId;
11247
+ window.visualViewport.addEventListener("resize", (event) => {
11248
+ visualViewportResizePending = true;
11249
+ clearTimeout(timeoutId);
11250
+ timeoutId = setTimeout(() => {
11251
+ visualViewportResizePending = false;
11252
+ publishVisualViewportResize(event);
11253
+ }, RESIZE_SETTLE_MS);
11254
+ });
11255
+ }
11256
+
11257
+ let windowResizeTimeoutId;
11258
+ window.addEventListener("resize", (event) => {
11259
+ clearTimeout(windowResizeTimeoutId);
11260
+ // Mobile browsers appear to dispatch visualViewport resize, then window
11261
+ // resize, then visualViewport resize again for the same keyboard/UI-chrome
11262
+ // shift — debounce the same way only when it looks like part of that
11263
+ // sequence (a visualViewport resize is already pending); otherwise react
11264
+ // immediately, so a genuine window resize isn't delayed for nothing.
11265
+ if (!visualViewportResizePending) {
11266
+ publishWindowResize(event);
11267
+ return;
11268
+ }
11269
+ windowResizeTimeoutId = setTimeout(() => {
11270
+ publishWindowResize(event);
11271
+ }, RESIZE_SETTLE_MS);
11272
+ });
11273
+
11103
11274
  // Minimum fraction of element width/height that must be visible on the preferred side
11104
11275
  // before flipping to the opposite side. Prevents flickering near the flip threshold.
11105
11276
  const MIN_CONTENT_VISIBILITY_RATIO = 0.6;
@@ -11148,7 +11319,14 @@ const visibleRectEffect = (
11148
11319
  } = {},
11149
11320
  ) => {
11150
11321
  const [teardown, addTeardown] = createPubSub();
11151
- const scrollContainer = getScrollContainer(element);
11322
+ // getScrollContainer(document.documentElement) returns null specifically
11323
+ // when the document itself has no overflow to scroll (e.g. a small
11324
+ // dialog/popover on an otherwise short page) — document.documentElement
11325
+ // is still a perfectly valid fallback in that case (scrollLeft/scrollTop
11326
+ // are just 0), so this never needs to crash the way a bare
11327
+ // `getScrollContainer(element)` result would below.
11328
+ const scrollContainer =
11329
+ getScrollContainer(element) ?? document.documentElement;
11152
11330
  const scrollContainerIsDocument =
11153
11331
  scrollContainer === document.documentElement;
11154
11332
  let lastMeasuredWidth;
@@ -11299,16 +11477,16 @@ const visibleRectEffect = (
11299
11477
  check(initialEvent);
11300
11478
 
11301
11479
  const [publishBeforeAutoCheck, onBeforeAutoCheck] = createPubSub();
11302
- {
11303
- const autoCheck = (event) => {
11304
- const beforeCheckResults = publishBeforeAutoCheck(event);
11305
- check(event);
11306
- for (const beforeCheckResult of beforeCheckResults) {
11307
- if (typeof beforeCheckResult === "function") {
11308
- beforeCheckResult();
11309
- }
11480
+ const autoCheck = (event) => {
11481
+ const beforeCheckResults = publishBeforeAutoCheck(event);
11482
+ check(event);
11483
+ for (const beforeCheckResult of beforeCheckResults) {
11484
+ if (typeof beforeCheckResult === "function") {
11485
+ beforeCheckResult();
11310
11486
  }
11311
- };
11487
+ }
11488
+ };
11489
+ {
11312
11490
  // let rafId = null;
11313
11491
  // const scheduleCheck = (reason) => {
11314
11492
  // cancelAnimationFrame(rafId);
@@ -11348,26 +11526,6 @@ const visibleRectEffect = (
11348
11526
  });
11349
11527
  }
11350
11528
  }
11351
- {
11352
- // visualViewport resize fires when the virtual keyboard opens/closes on mobile.
11353
- // window resize catches cases visualViewport misses, notably the browser input
11354
- // accessory bar (password/autocomplete suggestions) on iOS Safari which changes
11355
- // the available height without always firing a visualViewport resize event.
11356
- // We listen to both simultaneously; the check is idempotent so duplicates are harmless.
11357
- const onResize = (e) => {
11358
- autoCheck(e);
11359
- };
11360
- if (window.visualViewport) {
11361
- window.visualViewport.addEventListener("resize", onResize);
11362
- addTeardown(() => {
11363
- window.visualViewport.removeEventListener("resize", onResize);
11364
- });
11365
- }
11366
- window.addEventListener("resize", onResize);
11367
- addTeardown(() => {
11368
- window.removeEventListener("resize", onResize);
11369
- });
11370
- }
11371
11529
  {
11372
11530
  // visualViewport scroll fires when the visual viewport pans independently
11373
11531
  // of the layout viewport (e.g. during pinch-zoom). This is distinct from
@@ -11388,6 +11546,12 @@ const visibleRectEffect = (
11388
11546
  });
11389
11547
  }
11390
11548
  }
11549
+ {
11550
+ // See window_size.js's own module comment for why both of these go
11551
+ // through their shared debounce instead of each keeping its own timer.
11552
+ addTeardown(subscribeVisualViewportResizeSettled(autoCheck));
11553
+ addTeardown(subscribeWindowResizeSettled(autoCheck));
11554
+ }
11391
11555
  on_element_resize: {
11392
11556
  if (skipElementResize) {
11393
11557
  break on_element_resize;
@@ -11529,8 +11693,20 @@ const visibleRectEffect = (
11529
11693
  }
11530
11694
  };
11531
11695
  ancestor.addEventListener("toggle", onToggle);
11696
+
11697
+ const onNaviPositionChange = (e) => {
11698
+ autoCheck(e);
11699
+ };
11700
+ ancestor.addEventListener(
11701
+ "navi_position_change",
11702
+ onNaviPositionChange,
11703
+ );
11532
11704
  addTeardown(() => {
11533
11705
  ancestor.removeEventListener("toggle", onToggle);
11706
+ ancestor.removeEventListener(
11707
+ "navi_position_change",
11708
+ onNaviPositionChange,
11709
+ );
11534
11710
  });
11535
11711
  }
11536
11712
  current = current.parentElement;
@@ -11538,97 +11714,451 @@ const visibleRectEffect = (
11538
11714
  }
11539
11715
  }
11540
11716
 
11717
+ // Re-checks whenever `elementToObserve` (some other element than the one
11718
+ // this effect tracks — e.g. a popover/callout's own content) changes size,
11719
+ // not just when `element` itself is scrolled/resized/re-anchored. Useful
11720
+ // when the tracked element's *position* depends on a size that lives
11721
+ // elsewhere (a callout re-measuring itself against its message body, a
11722
+ // popover reconsidering "top" vs "bottom" once its own content grows).
11723
+ // Can be called more than once, once per element worth watching.
11724
+ const observeSize = (elementToObserve) => {
11725
+ let lastWidth;
11726
+ let lastHeight;
11727
+ // Set right before a deferred check() runs, read right after — see
11728
+ // below for why a pending frame needs to be cancelable.
11729
+ let pendingFrame = null;
11730
+ const resizeObserver = new ResizeObserver((entries) => {
11731
+ const [entry] = entries;
11732
+ const { width, height } = entry.contentRect;
11733
+ // Debounce tiny changes that are likely sub-pixel rounding.
11734
+ if (lastWidth !== undefined) {
11735
+ const widthDiff = Math.abs(width - lastWidth);
11736
+ const heightDiff = Math.abs(height - lastHeight);
11737
+ const threshold = 1;
11738
+ if (widthDiff < threshold && heightDiff < threshold) {
11739
+ return;
11740
+ }
11741
+ }
11742
+ lastWidth = width;
11743
+ lastHeight = height;
11744
+ // Deferred to the next frame rather than calling check() here
11745
+ // directly: check() (via update()) commonly mutates
11746
+ // elementToObserve's own size again as a side effect of repositioning
11747
+ // it (e.g. a popover clearing then re-setting its own max-height
11748
+ // while reconsidering "top" vs "bottom" once it no longer fits where
11749
+ // it was) — when elementToObserve is the very element this observer
11750
+ // watches (a popover watching its own content, not some other
11751
+ // element), doing that synchronously from inside this callback is a
11752
+ // same-frame observer-triggers-itself loop, which the browser detects
11753
+ // and reports as "ResizeObserver loop completed with undelivered
11754
+ // notifications." The debounce above only guards against oscillation
11755
+ // across separate ResizeObserver deliveries — it does nothing for
11756
+ // this single legitimate resize-causes-a-reposition-causes-another-
11757
+ // resize step, since each individual size change here is real, not
11758
+ // sub-pixel noise. Deferring one frame breaks the synchronous chain:
11759
+ // by the time the reposition runs, this callback has already
11760
+ // returned, so any size change it causes is observed as a fresh,
11761
+ // later delivery instead of a nested one. Cancels/replaces any
11762
+ // still-pending frame from an earlier, superseded delivery, so only
11763
+ // the latest size ever actually gets checked.
11764
+ if (pendingFrame !== null) {
11765
+ cancelAnimationFrame(pendingFrame);
11766
+ }
11767
+ pendingFrame = requestAnimationFrame(() => {
11768
+ pendingFrame = null;
11769
+ check(
11770
+ new CustomEvent("observed_element_size_change", {
11771
+ detail: { width, height },
11772
+ }),
11773
+ );
11774
+ });
11775
+ });
11776
+ resizeObserver.observe(elementToObserve);
11777
+ const cleanupAutoCheck = onBeforeAutoCheck(() => {
11778
+ resizeObserver.unobserve(elementToObserve);
11779
+ return () => {
11780
+ resizeObserver.observe(elementToObserve);
11781
+ };
11782
+ });
11783
+ addTeardown(() => {
11784
+ if (pendingFrame !== null) {
11785
+ cancelAnimationFrame(pendingFrame);
11786
+ }
11787
+ resizeObserver.disconnect();
11788
+ });
11789
+ return () => {
11790
+ cleanupAutoCheck();
11791
+ if (pendingFrame !== null) {
11792
+ cancelAnimationFrame(pendingFrame);
11793
+ }
11794
+ resizeObserver.disconnect();
11795
+ };
11796
+ };
11797
+
11541
11798
  return {
11542
11799
  check,
11543
11800
  onBeforeAutoCheck,
11801
+ observeSize,
11544
11802
  disconnect: () => {
11545
11803
  teardown();
11546
11804
  },
11547
11805
  };
11548
11806
  };
11549
11807
 
11808
+ /**
11809
+ * The `positionArea` grammar `pickPositionRelativeTo` accepts (also reused
11810
+ * as-is by `@jsenv/navi`'s Popover/Dialog/Callout): a single compass token
11811
+ * (loosely inspired by CSS `position-area`'s own naming), optionally wrapped
11812
+ * in `inset(...)` when the element should overlap the anchor instead of
11813
+ * sitting fully to one side of it. Resolves internally to a { y, x } pair —
11814
+ * y: top/inset-top/center/inset-bottom/bottom, x: left/inset-left/center/
11815
+ * inset-right/right — the same vocabulary the rest of this file's
11816
+ * positioning math (spaceFor, oppositeX/Y, etc.) actually operates on: a
11817
+ * bare `top`/`bottom`/`left`/`right` means outside the anchor (no overlap on
11818
+ * that axis), `inset-*` means flush against/overlapping it.
11819
+ *
11820
+ * Outside the anchor (bare token — element placed fully to one side, no
11821
+ * overlap on that side's axis):
11822
+ *
11823
+ * top-left top-start top top-end top-right
11824
+ * right-start right right-end
11825
+ * bottom-right bottom-end bottom bottom-start bottom-left
11826
+ * left-end left left-start
11827
+ *
11828
+ * A corner token fixes one axis outside (top/bottom/left/right) and the
11829
+ * other the same way (a true corner, no cross-axis overlap at all).
11830
+ * "-start"/"-end" keep one axis outside but align the cross axis flush with
11831
+ * the anchor's near/far edge instead (`top-start` is above the anchor,
11832
+ * left-edges flush). The bare direction word centers the cross axis on the
11833
+ * anchor.
11834
+ *
11835
+ * Overlapping the anchor (wrapped in `inset(...)`, the classic 3×3 grid):
11836
+ *
11837
+ * inset(top-left) inset(top) inset(top-right)
11838
+ * inset(left) center inset(right)
11839
+ * inset(bottom-left) inset(bottom) inset(bottom-right)
11840
+ *
11841
+ * `center` and `inset(center)` are equivalent aliases for dead-center.
11842
+ */
11843
+ const OUTSIDE_POSITION_AREA_TOKENS = {
11844
+ "top-left": { y: "top", x: "left" },
11845
+ "top-start": { y: "top", x: "inset-left" },
11846
+ "top": { y: "top", x: "center" },
11847
+ "top-end": { y: "top", x: "inset-right" },
11848
+ "top-right": { y: "top", x: "right" },
11849
+
11850
+ "right-start": { y: "inset-top", x: "right" },
11851
+ "right": { y: "center", x: "right" },
11852
+ "right-end": { y: "inset-bottom", x: "right" },
11853
+
11854
+ "bottom-right": { y: "bottom", x: "right" },
11855
+ "bottom-end": { y: "bottom", x: "inset-right" },
11856
+ "bottom": { y: "bottom", x: "center" },
11857
+ "bottom-start": { y: "bottom", x: "inset-left" },
11858
+ "bottom-left": { y: "bottom", x: "left" },
11859
+
11860
+ "left-end": { y: "inset-bottom", x: "left" },
11861
+ "left": { y: "center", x: "left" },
11862
+ "left-start": { y: "inset-top", x: "left" },
11863
+
11864
+ "center": { y: "center", x: "center" },
11865
+ };
11866
+ const INSET_POSITION_AREA_TOKENS = {
11867
+ "top-left": { y: "inset-top", x: "inset-left" },
11868
+ "top": { y: "inset-top", x: "center" },
11869
+ "top-right": { y: "inset-top", x: "inset-right" },
11870
+
11871
+ "right": { y: "center", x: "inset-right" },
11872
+
11873
+ "bottom-right": { y: "inset-bottom", x: "inset-right" },
11874
+ "bottom": { y: "inset-bottom", x: "center" },
11875
+ "bottom-left": { y: "inset-bottom", x: "inset-left" },
11876
+
11877
+ "left": { y: "center", x: "inset-left" },
11878
+
11879
+ "center": { y: "center", x: "center" },
11880
+ };
11881
+ const INSET_TOKEN_RE = /^inset\(\s*([a-z-]+)\s*\)$/;
11882
+
11883
+ /**
11884
+ * Parses a positionArea string into a { y, x } pair, or null if it's not a
11885
+ * recognized token.
11886
+ */
11887
+ const parsePositionArea = (value) => {
11888
+ const insetMatch = INSET_TOKEN_RE.exec(value);
11889
+ if (insetMatch) {
11890
+ const parsed = INSET_POSITION_AREA_TOKENS[insetMatch[1]];
11891
+ return parsed ? { ...parsed } : null;
11892
+ }
11893
+ const parsed = OUTSIDE_POSITION_AREA_TOKENS[value];
11894
+ return parsed ? { ...parsed } : null;
11895
+ };
11896
+
11897
+ /**
11898
+ * Collapses a bare position value ("top"/"bottom"/"left"/"right") to its
11899
+ * "inset-*" equivalent — "inset-*"/"center" values pass through unchanged.
11900
+ * Only used by pickPositionRelativeTo's own no-anchor (container-docked)
11901
+ * mode — see its own doc for why.
11902
+ */
11903
+ const toContainerAlignedPosition = (value) => {
11904
+ if (value === "top") {
11905
+ return "inset-top";
11906
+ }
11907
+ if (value === "bottom") {
11908
+ return "inset-bottom";
11909
+ }
11910
+ if (value === "left") {
11911
+ return "inset-left";
11912
+ }
11913
+ if (value === "right") {
11914
+ return "inset-right";
11915
+ }
11916
+ return value;
11917
+ };
11918
+
11550
11919
  /**
11551
11920
  * Places element relative to anchor with independent control of horizontal and vertical axes.
11552
11921
  *
11553
- * Horizontal axis positionX / positionXFixed (left right):
11554
- * "to-the-left" element.right = anchor.left (sits entirely to the left of anchor)
11555
- * "left-aligned" element.left = anchor.left (left edges aligned)
11556
- * "center" element centered horizontally over anchor (default)
11557
- * "right-aligned" element.right = anchor.right (right edges aligned)
11558
- * "to-the-right" element.left = anchor.right (sits entirely to the right of anchor)
11559
- *
11560
- * Vertical axis positionY / positionYFixed (top → bottom):
11561
- * "above" element.bottom = anchor.top (sits above, no overlap)
11562
- * "above-overlap" element.bottom = anchor.bottom (sits above, overlapping anchor)
11563
- * "center" element centered vertically over anchor
11564
- * "below-overlap" element.top = anchor.top (sits below, overlapping anchor)
11565
- * "below" element.top = anchor.bottom (sits below, no overlap) (default)
11566
- *
11567
- * positionX / positionY attempt the requested placement and automatically flip to the
11922
+ * `positionArea` (see its own doc above `parsePositionArea`) is a single
11923
+ * compass token that resolves to a { y, x } pair internally:
11924
+ *
11925
+ * Horizontal (x) axis:
11926
+ * "left" element.right = anchor.left (sits entirely to the left of anchor)
11927
+ * "inset-left" element.left = anchor.left (left edges aligned, overlapping)
11928
+ * "center" element centered horizontally over anchor
11929
+ * "inset-right" element.right = anchor.right (right edges aligned, overlapping)
11930
+ * "right" element.left = anchor.right (sits entirely to the right of anchor)
11931
+ *
11932
+ * Vertical (y) axis:
11933
+ * "top" element.bottom = anchor.top (sits above, no overlap)
11934
+ * "inset-top" element.top = anchor.top (top edges aligned, overlapping)
11935
+ * "center" element centered vertically over anchor
11936
+ * "inset-bottom" element.bottom = anchor.bottom (bottom edges aligned, overlapping)
11937
+ * "bottom" element.top = anchor.bottom (sits below, no overlap)
11938
+ *
11939
+ * The resolved x/y attempt the requested placement and automatically flip to the
11568
11940
  * logical opposite when the element does not fit in the viewport:
11569
- * abovebelow, above-overlapbelow-overlap
11941
+ * topbottom, inset-topinset-bottom, left ↔ right, inset-left ↔ inset-right
11570
11942
  *
11571
- * positionXFixed / positionYFixed skip the fit check entirely.
11943
+ * `positionAreaFixed` skips the fit check entirely on both axes.
11572
11944
  *
11573
11945
  * The resolved X and Y are persisted as data-position-x-current / data-position-y-current
11574
11946
  * on the element so subsequent calls start from the last resolved position (avoids
11575
- * flickering when the element is near the flip threshold). Fixed axes are not persisted.
11576
- *
11577
- * @param {HTMLElement} element - The element to position (must be document-relative)
11578
- * @param {HTMLElement} anchor - The anchor element to position against
11947
+ * flickering when the element is near the flip threshold) and so other CSS/JS can read
11948
+ * "which side is this on right now" — including for a fixed axis, even though a fixed
11949
+ * axis never reads the attribute back itself (`positionAreaFixed` always wins).
11950
+ *
11951
+ * @param {HTMLElement} element - The element to position (position: absolute or
11952
+ * fixed — detected from its own computed style, see the scroll offset comment below)
11953
+ * @param {HTMLElement} [anchor] - The anchor element to position against. Omit (or pass
11954
+ * `null`/`undefined`) when there's no real anchor to dock `element` against a *container*
11955
+ * instead — see `container` below; in that mode, "top"/"bottom"/"left"/"right" are
11956
+ * collapsed to their "inset-*" equivalent internally (docking has no "float away with
11957
+ * a gap" concept the way a real anchor does) and x/y always behave as if
11958
+ * `positionAreaFixed` were set (a docked edge/corner never flips to the other side —
11959
+ * there's no "other side" of a container the way there is of a real anchor).
11579
11960
  * @param {object} [options]
11580
- * @param {string} [options.positionX="center"] - Preferred X placement, with viewport fallback.
11581
- * "to-the-left" element.right = anchor.left (sits entirely to the left of anchor)
11582
- * "left-aligned" — element.left = anchor.left (left edges aligned)
11583
- * "center" — element centered horizontally over anchor (default)
11584
- * "right-aligned" element.right = anchor.right (right edges aligned)
11585
- * "to-the-right" — element.left = anchor.right (sits entirely to the right of anchor)
11586
- * @param {string} [options.positionY="below"] - Preferred Y placement, with viewport fallback.
11587
- * "above" — element.bottom = anchor.top (sits above, no overlap)
11588
- * "above-overlap" element.bottom = anchor.bottom (sits above, overlapping anchor)
11589
- * "center" — element centered vertically over anchor
11590
- * "below-overlap" element.top = anchor.top (sits below, overlapping anchor)
11591
- * "below" — element.top = anchor.bottom (sits below, no overlap) (default)
11592
- * @param {string} [options.positionXFixed] - Force X placement, skipping the fit-check. Same values as positionX.
11593
- * @param {string} [options.positionYFixed] - Force Y placement, skipping the fit-check. Same values as positionY.
11594
- * @param {number} [options.alignToViewportEdgeWhenAnchorNearEdge=0] - Snap to viewport left
11595
- * edge when anchor is within this many px of the left edge and element is wider than anchor.
11961
+ * @param {string} [options.positionArea="bottom"] - Preferred placement, with viewport
11962
+ * fallback see `parsePositionArea`'s own doc for the full token grammar (a single
11963
+ * compass token, optionally `inset(...)`-wrapped).
11964
+ * @param {string} [options.positionAreaFixed] - Forces this placement, skipping the
11965
+ * fit-check on both axes. Same grammar as `positionArea`.
11966
+ * @param {string} [options.positionAreaWhenAnchorIsInvalid="center"] - `positionArea`
11967
+ * used instead, as a plain no-anchor dock, whenever the anchor is too big to leave
11968
+ * room on the axis `positionArea` places it outside of. `hasValidAnchor` in the return
11969
+ * value reports which way it went.
11970
+ * @param {Event|CustomEvent} [options.event] - The event that triggered this particular
11971
+ * reposition (a scroll/resize/etc. handler simply forwarding whatever it was itself
11972
+ * called with) purely informational, never changes the computed `left`/`top`
11973
+ * themselves, only `shouldTransition` in the return value (see `applyNewPosition`'s
11974
+ * own doc for how that's meant to be used).
11975
+ * @param {number} [options.alignToContainerEdgeWhenAnchorNearEdge=0] - When centering
11976
+ * (positionArea's x is "center") an element wider than its anchor, snap to the available area's own
11977
+ * left edge (the page viewport normally, or the container's edge — see `container` below —
11978
+ * whenever there's no real `anchor`) instead of centering, once the anchor is within this
11979
+ * many px of that same edge — avoids the (wider) element overflowing past it. 0 disables
11980
+ * the snap entirely.
11596
11981
  * @param {number} [options.minLeft=0] - Minimum left coordinate (document-relative).
11597
- * @returns {{ positionX, positionY, left, top, width, height, anchorLeft, anchorTop, anchorRight, anchorBottom, spaceLeft, spaceRight, spaceAbove, spaceBelow }}
11982
+ * @param {HTMLElement|null} [options.container] - The container `element` is genuinely
11983
+ * `position: absolute` relative to (its own containing block) — decoupled from whether
11984
+ * there's a real `anchor`, since `element` can be container-relative either way (e.g. the
11985
+ * custom renderer in popover.jsx, always relative to its own positioned ancestor whether
11986
+ * or not it also has a real anchor). Whenever not explicitly given, this is always
11987
+ * resolved automatically via `getPositioningContainer(element)` instead — regardless of
11988
+ * `hasValidAnchor` — so a caller that never thinks about `container` at all still gets the
11989
+ * right behavior on its own: `null` from `getPositioningContainer` (an `element` with a
11990
+ * `popover` attribute, or a `<dialog>` — e.g. Callout's own element) falls back to the
11991
+ * traditional document-relative path below, exactly as if `container` genuinely didn't
11992
+ * apply; anything else `getPositioningContainer` finds (a real positioned ancestor) is
11993
+ * used the same way an explicit `container` would be. A container that resolves to
11994
+ * `document.documentElement` (the viewport) produces identical output to the plain
11995
+ * document-relative path either way, since the document's own scroll and the viewport's
11996
+ * own origin already coincide with what this generically computes for any other container
11997
+ * element. When there's a real container (explicit or resolved) either way: the final
11998
+ * `left`/`top` (and the returned `anchorLeft/Top/Right/Bottom`) are expressed relative to
11999
+ * its own padding-box origin plus its own scroll, instead of the document's — `element`'s
12000
+ * own computed `position` is *not* consulted in that case, unlike the traditional path.
12001
+ * When `anchor` is also omitted (no real anchor at all), the container additionally
12002
+ * becomes what's positioned against, and the boundary clamp uses its own (padding-box)
12003
+ * edges instead of the page viewport's, on both axes (the Y axis otherwise has no such
12004
+ * clamp at all — see the clamp's own comment) — that part *is* gated on `hasValidAnchor`,
12005
+ * unlike the coordinate-space conversion itself.
12006
+ * @returns {{ hasValidAnchor, shouldTransition, positionX, positionY, left, top, width, height, anchorLeft, anchorTop, anchorRight, anchorBottom, spaceLeft, spaceRight, spaceAbove, spaceBelow }}
11598
12007
  */
11599
12008
  const pickPositionRelativeTo = (
11600
12009
  element,
11601
12010
  anchor,
11602
12011
  {
11603
- positionX = "center",
11604
- positionY = "below",
11605
- positionXFixed,
11606
- positionYFixed,
11607
- alignToViewportEdgeWhenAnchorNearEdge = 0,
12012
+ positionArea = "bottom",
12013
+ positionAreaFixed,
12014
+ positionAreaWhenAnchorIsInvalid = "center",
12015
+ event,
12016
+ alignToContainerEdgeWhenAnchorNearEdge = 0,
11608
12017
  minLeft = 0,
11609
- spacing = 0,
12018
+ marginWithAnchor = 0,
11610
12019
  alignToAnchorBox = "border-box",
11611
- viewportSpacing = 0,
12020
+ marginWithContainer = 0,
12021
+ container,
11612
12022
  } = {},
11613
12023
  ) => {
11614
-
11615
- const viewportWidth = document.documentElement.clientWidth;
11616
- const viewportHeight = document.documentElement.clientHeight;
12024
+ // Needed before hasValidAnchor below. visualViewport, not
12025
+ // document.documentElement.clientWidth/Height: the layout viewport
12026
+ // doesn't shrink when the on-screen keyboard opens, only the visual one
12027
+ // does.
12028
+ const visualViewport = window.visualViewport;
12029
+ const viewportWidth = visualViewport
12030
+ ? visualViewport.width
12031
+ : document.documentElement.clientWidth;
12032
+ const viewportHeight = visualViewport
12033
+ ? visualViewport.height
12034
+ : document.documentElement.clientHeight;
12035
+ const viewportLeft = visualViewport ? visualViewport.offsetLeft : 0;
12036
+ const viewportTop = visualViewport ? visualViewport.offsetTop : 0;
12037
+
12038
+ // Resolved early: everything below that would otherwise reach for
12039
+ // viewportLeft/Top/Width/Height instead uses these, so a "local" popover
12040
+ // never gets offered more room (anchor-too-big check, flip decisions,
12041
+ // clamp) than its own container — resolvedContainer's own padding-box
12042
+ // edges when there is one — actually has.
12043
+ const resolvedContainer = container ?? getPositioningContainer(element);
12044
+ const hasRealContainer =
12045
+ resolvedContainer && resolvedContainer !== document.documentElement;
12046
+ const containerRect = hasRealContainer
12047
+ ? resolvedContainer.getBoundingClientRect()
12048
+ : null;
12049
+ const containerBorders = hasRealContainer
12050
+ ? getBorderSizes(resolvedContainer)
12051
+ : { left: 0, top: 0, right: 0, bottom: 0 };
12052
+ const availableLeft = hasRealContainer
12053
+ ? snapToPixel(containerRect.left) + containerBorders.left
12054
+ : viewportLeft;
12055
+ const availableTop = hasRealContainer
12056
+ ? snapToPixel(containerRect.top) + containerBorders.top
12057
+ : viewportTop;
12058
+ const availableRight = hasRealContainer
12059
+ ? snapToPixel(containerRect.right) - containerBorders.right
12060
+ : viewportLeft + viewportWidth;
12061
+ const availableBottom = hasRealContainer
12062
+ ? snapToPixel(containerRect.bottom) - containerBorders.bottom
12063
+ : viewportTop + viewportHeight;
12064
+ const availableWidth = availableRight - availableLeft;
12065
+ const availableHeight = availableBottom - availableTop;
12066
+
12067
+ // Rejected only on the axis positionArea actually places `element`
12068
+ // outside of ("left"/"right" or "top"/"bottom") — that's the only axis
12069
+ // where the anchor's own size eats into the room available. Docks via
12070
+ // positionAreaWhenAnchorIsInvalid instead of `positionArea` once rejected.
12071
+ const requestedPositionArea = parsePositionArea(positionArea);
12072
+ const anchorRejected =
12073
+ Boolean(anchor) &&
12074
+ (() => {
12075
+ const rect = anchor.getBoundingClientRect();
12076
+ const { x, y } = requestedPositionArea ?? {};
12077
+ if (
12078
+ (y === "top" || y === "bottom") &&
12079
+ rect.height > availableHeight - 50
12080
+ ) {
12081
+ return true;
12082
+ }
12083
+ if ((x === "left" || x === "right") && rect.width > availableWidth - 50) {
12084
+ return true;
12085
+ }
12086
+ return false;
12087
+ })();
12088
+ const hasValidAnchor = Boolean(anchor) && !anchorRejected;
12089
+ const effectivePositionArea = anchorRejected
12090
+ ? positionAreaWhenAnchorIsInvalid
12091
+ : positionArea;
12092
+
12093
+ const parsedPositionArea = parsePositionArea(effectivePositionArea);
12094
+ if (!parsedPositionArea) {
12095
+ console.warn(
12096
+ `pickPositionRelativeTo: invalid positionArea="${effectivePositionArea}"`,
12097
+ );
12098
+ }
12099
+ let positionX = parsedPositionArea ? parsedPositionArea.x : "center";
12100
+ let positionY = parsedPositionArea ? parsedPositionArea.y : "bottom";
12101
+ let positionXFixed;
12102
+ let positionYFixed;
12103
+ if (positionAreaFixed) {
12104
+ const parsedPositionAreaFixed = parsePositionArea(positionAreaFixed);
12105
+ if (!parsedPositionAreaFixed) {
12106
+ console.warn(
12107
+ `pickPositionRelativeTo: invalid positionAreaFixed="${positionAreaFixed}"`,
12108
+ );
12109
+ } else {
12110
+ positionXFixed = parsedPositionAreaFixed.x;
12111
+ positionYFixed = parsedPositionAreaFixed.y;
12112
+ }
12113
+ }
12114
+ // No real anchor (or a rejected one): dock against a container instead.
12115
+ if (!hasValidAnchor) {
12116
+ positionX = toContainerAlignedPosition(positionX);
12117
+ positionY = toContainerAlignedPosition(positionY);
12118
+ positionXFixed = positionX;
12119
+ positionYFixed = positionY;
12120
+ }
12121
+ // resolvedContainer was already resolved above. `null` from
12122
+ // getPositioningContainer (a popover/dialog element, e.g. Callout's own)
12123
+ // falls through to the traditional document-relative path below all the
12124
+ // same, so an existing caller that never thinks about `container` at all
12125
+ // keeps behaving exactly as before.
12126
+ const effectiveAnchor = hasValidAnchor
12127
+ ? anchor
12128
+ : resolvedContainer || document.documentElement;
12129
+ // document.documentElement is used as a sentinel "the viewport" value: an
12130
+ // anchorless popup should center/place itself against the visual
12131
+ // viewport, not against <html>'s own box — which, unlike the viewport,
12132
+ // grows with document content and can be far taller than what's on
12133
+ // screen (its top is also negative once the page is scrolled). Using the
12134
+ // viewport rect here fixes that; the scroll offset is still applied
12135
+ // below like any other case (see getPositioningScrollOffset).
12136
+ const anchorIsViewport = effectiveAnchor === document.documentElement;
11617
12137
  // Get viewport-relative positions
11618
- const elementRect = element.getBoundingClientRect();
11619
- const anchorRect = anchor.getBoundingClientRect();
11620
- const {
11621
- left: elementLeft,
11622
- right: elementRight,
11623
- top: elementTop,
11624
- bottom: elementBottom,
11625
- } = elementRect;
12138
+ const anchorRect = anchorIsViewport
12139
+ ? {
12140
+ left: viewportLeft,
12141
+ top: viewportTop,
12142
+ right: viewportLeft + viewportWidth,
12143
+ bottom: viewportTop + viewportHeight,
12144
+ }
12145
+ : effectiveAnchor.getBoundingClientRect();
11626
12146
  const anchorLeft = snapToPixel(anchorRect.left);
11627
12147
  const anchorTop = snapToPixel(anchorRect.top);
11628
12148
  const anchorRight = snapToPixel(anchorRect.right);
11629
12149
  const anchorBottom = snapToPixel(anchorRect.bottom);
11630
- const elementWidth = elementRight - elementLeft;
11631
- const elementHeight = elementBottom - elementTop;
12150
+ // Horizontal clamp bounds see availableLeft/availableRight above.
12151
+ const clampLeftBound = availableLeft;
12152
+ const clampRightBound = availableRight;
12153
+ // offsetWidth/offsetHeight (layout box), not getBoundingClientRect() (the
12154
+ // painted/transformed box): the element being positioned may have an
12155
+ // active CSS `scale`/`translate` transform mid-animation (e.g. a popover
12156
+ // using animation="scale"/"grow", still at its @starting-style value the
12157
+ // instant it's first shown) — getBoundingClientRect() would then report
12158
+ // its *shrunk* transformed size, throwing off any math that centers/fits
12159
+ // against the element's own dimensions.
12160
+ const elementWidth = element.offsetWidth;
12161
+ const elementHeight = element.offsetHeight;
11632
12162
  const anchorWidth = anchorRight - anchorLeft;
11633
12163
  const anchorHeight = anchorBottom - anchorTop;
11634
12164
 
@@ -11643,19 +12173,19 @@ const pickPositionRelativeTo = (
11643
12173
  let insetLeft = 0;
11644
12174
  let insetRight = 0;
11645
12175
  if (alignToAnchorBox === "content-box") {
11646
- const anchorBorderSizes = getBorderSizes(anchor);
11647
- const anchorPaddingSizes = getPaddingSizes(anchor);
12176
+ const anchorBorderSizes = getBorderSizes(effectiveAnchor);
12177
+ const anchorPaddingSizes = getPaddingSizes(effectiveAnchor);
11648
12178
  insetTop = anchorBorderSizes.top + anchorPaddingSizes.top;
11649
12179
  insetBottom = anchorBorderSizes.bottom + anchorPaddingSizes.bottom;
11650
12180
  insetLeft = anchorBorderSizes.left + anchorPaddingSizes.left;
11651
12181
  insetRight = anchorBorderSizes.right + anchorPaddingSizes.right;
11652
12182
  }
11653
- const spaceAbove = anchorTop + insetTop;
11654
- const spaceBelow = viewportHeight - anchorBottom + insetBottom;
12183
+ const spaceAbove = anchorTop + insetTop - availableTop;
12184
+ const spaceBelow = availableBottom - anchorBottom + insetBottom;
11655
12185
  const effectiveAnchorLeft = anchorLeft + insetLeft;
11656
12186
  const effectiveAnchorRight = anchorRight - insetRight;
11657
- const spaceLeft = anchorLeft + insetLeft;
11658
- const spaceRight = viewportWidth - anchorRight + insetRight;
12187
+ const spaceLeft = anchorLeft + insetLeft - availableLeft;
12188
+ const spaceRight = availableRight - anchorRight + insetRight;
11659
12189
 
11660
12190
  // Resolve active X and Y, and whether each is fixed (no flip fallback)
11661
12191
  let activeX;
@@ -11681,24 +12211,24 @@ const pickPositionRelativeTo = (
11681
12211
  let finalY;
11682
12212
  {
11683
12213
  const oppositeY = {
11684
- "above": "below",
11685
- "below": "above",
11686
- "above-overlap": "below-overlap",
11687
- "below-overlap": "above-overlap",
12214
+ "top": "bottom",
12215
+ "bottom": "top",
12216
+ "inset-top": "inset-bottom",
12217
+ "inset-bottom": "inset-top",
11688
12218
  };
11689
12219
  // Compute effective space for a given Y value
11690
12220
  const spaceFor = (y) => {
11691
- if (y === "above") {
11692
- return spaceAbove - spacing - viewportSpacing;
12221
+ if (y === "top") {
12222
+ return spaceAbove - marginWithAnchor - marginWithContainer;
11693
12223
  }
11694
- if (y === "above-overlap") {
11695
- return spaceAbove + anchorHeight - viewportSpacing;
12224
+ if (y === "inset-bottom") {
12225
+ return spaceAbove + anchorHeight - marginWithContainer;
11696
12226
  }
11697
- if (y === "below") {
11698
- return spaceBelow - spacing - viewportSpacing;
12227
+ if (y === "bottom") {
12228
+ return spaceBelow - marginWithAnchor - marginWithContainer;
11699
12229
  }
11700
- if (y === "below-overlap") {
11701
- return spaceBelow + anchorHeight - viewportSpacing;
12230
+ if (y === "inset-top") {
12231
+ return spaceBelow + anchorHeight - marginWithContainer;
11702
12232
  }
11703
12233
  return Infinity; // center
11704
12234
  };
@@ -11742,24 +12272,24 @@ const pickPositionRelativeTo = (
11742
12272
  let finalX;
11743
12273
  {
11744
12274
  const oppositeX = {
11745
- "to-the-left": "to-the-right",
11746
- "to-the-right": "to-the-left",
11747
- "left-aligned": "right-aligned",
11748
- "right-aligned": "left-aligned",
12275
+ "left": "right",
12276
+ "right": "left",
12277
+ "inset-left": "inset-right",
12278
+ "inset-right": "inset-left",
11749
12279
  };
11750
12280
  // Compute effective space for a given X value
11751
12281
  const spaceFor = (x) => {
11752
- if (x === "to-the-left") {
11753
- return spaceLeft - spacing - viewportSpacing;
12282
+ if (x === "left") {
12283
+ return spaceLeft - marginWithAnchor - marginWithContainer;
11754
12284
  }
11755
- if (x === "left-aligned") {
11756
- return viewportWidth - anchorLeft - viewportSpacing;
12285
+ if (x === "inset-left") {
12286
+ return availableRight - anchorLeft - marginWithContainer;
11757
12287
  }
11758
- if (x === "right-aligned") {
11759
- return anchorRight - viewportSpacing;
12288
+ if (x === "inset-right") {
12289
+ return anchorRight - availableLeft - marginWithContainer;
11760
12290
  }
11761
- if (x === "to-the-right") {
11762
- return spaceRight - spacing - viewportSpacing;
12291
+ if (x === "right") {
12292
+ return spaceRight - marginWithAnchor - marginWithContainer;
11763
12293
  }
11764
12294
  return Infinity; // center
11765
12295
  };
@@ -11795,101 +12325,178 @@ const pickPositionRelativeTo = (
11795
12325
  // Calculate horizontal position (viewport-relative)
11796
12326
  let elementPositionLeft;
11797
12327
  {
11798
- if (finalX === "to-the-left") {
11799
- elementPositionLeft = effectiveAnchorLeft - elementWidth - spacing;
11800
- } else if (finalX === "left-aligned") {
12328
+ if (finalX === "left") {
12329
+ elementPositionLeft =
12330
+ effectiveAnchorLeft - elementWidth - marginWithAnchor;
12331
+ } else if (finalX === "inset-left") {
11801
12332
  elementPositionLeft = effectiveAnchorLeft;
11802
12333
  } else if (finalX === "center") {
11803
- // Complex logic handles wide anchors and viewport-edge snapping
11804
- const anchorIsWiderThanViewport = anchorWidth > viewportWidth;
11805
- if (anchorIsWiderThanViewport) {
11806
- const anchorLeftIsVisible = effectiveAnchorLeft >= 0;
11807
- const anchorRightIsVisible = effectiveAnchorRight <= viewportWidth;
12334
+ // Complex logic handles wide anchors and container-edge snapping
12335
+ const anchorIsWiderThanAvailable = anchorWidth > availableWidth;
12336
+ if (anchorIsWiderThanAvailable) {
12337
+ const anchorLeftIsVisible = effectiveAnchorLeft >= availableLeft;
12338
+ const anchorRightIsVisible = effectiveAnchorRight <= availableRight;
11808
12339
  if (!anchorLeftIsVisible && anchorRightIsVisible) {
11809
- const viewportCenter = viewportWidth / 2;
11810
- const distanceFromRightEdge = viewportWidth - effectiveAnchorRight;
12340
+ const availableCenter = availableLeft + availableWidth / 2;
12341
+ const distanceFromRightEdge = availableRight - effectiveAnchorRight;
11811
12342
  elementPositionLeft =
11812
- viewportCenter - distanceFromRightEdge / 2 - elementWidth / 2;
12343
+ availableCenter - distanceFromRightEdge / 2 - elementWidth / 2;
11813
12344
  } else if (anchorLeftIsVisible && !anchorRightIsVisible) {
11814
- const viewportCenter = viewportWidth / 2;
11815
- const distanceFromLeftEdge = -effectiveAnchorLeft;
12345
+ const availableCenter = availableLeft + availableWidth / 2;
12346
+ const distanceFromLeftEdge = availableLeft - effectiveAnchorLeft;
11816
12347
  elementPositionLeft =
11817
- viewportCenter - distanceFromLeftEdge / 2 - elementWidth / 2;
12348
+ availableCenter - distanceFromLeftEdge / 2 - elementWidth / 2;
11818
12349
  } else {
11819
- elementPositionLeft = viewportWidth / 2 - elementWidth / 2;
12350
+ elementPositionLeft =
12351
+ availableLeft + availableWidth / 2 - elementWidth / 2;
11820
12352
  }
11821
12353
  } else {
11822
12354
  elementPositionLeft =
11823
12355
  effectiveAnchorLeft +
11824
12356
  (effectiveAnchorRight - effectiveAnchorLeft) / 2 -
11825
12357
  elementWidth / 2;
11826
- if (alignToViewportEdgeWhenAnchorNearEdge) {
12358
+ if (alignToContainerEdgeWhenAnchorNearEdge) {
11827
12359
  const effectiveAnchorWidth =
11828
12360
  effectiveAnchorRight - effectiveAnchorLeft;
11829
12361
  const elementIsWiderThanAnchor = elementWidth > effectiveAnchorWidth;
11830
- const anchorIsNearLeftEdge =
11831
- effectiveAnchorLeft < alignToViewportEdgeWhenAnchorNearEdge;
11832
- if (elementIsWiderThanAnchor && anchorIsNearLeftEdge) {
11833
- elementPositionLeft = minLeft;
12362
+ const anchorIsNearContainerEdge =
12363
+ effectiveAnchorLeft - clampLeftBound <
12364
+ alignToContainerEdgeWhenAnchorNearEdge;
12365
+ if (elementIsWiderThanAnchor && anchorIsNearContainerEdge) {
12366
+ elementPositionLeft = clampLeftBound + minLeft;
11834
12367
  }
11835
12368
  }
11836
12369
  }
11837
- } else if (finalX === "right-aligned") {
12370
+ } else if (finalX === "inset-right") {
11838
12371
  elementPositionLeft = effectiveAnchorRight - elementWidth;
11839
12372
  } else {
11840
- // "to-the-right"
11841
- elementPositionLeft = effectiveAnchorRight + spacing;
12373
+ // "right"
12374
+ elementPositionLeft = effectiveAnchorRight + marginWithAnchor;
11842
12375
  }
11843
- // Constrain horizontal position to viewport boundaries (with viewportSpacing margin)
11844
- if (elementPositionLeft < viewportSpacing) {
11845
- elementPositionLeft = viewportSpacing;
12376
+ // Constrain horizontal position to the available area's boundaries
12377
+ // (with marginWithContainer margin).
12378
+ if (elementPositionLeft < clampLeftBound + marginWithContainer) {
12379
+ elementPositionLeft = clampLeftBound + marginWithContainer;
11846
12380
  } else if (
11847
12381
  elementPositionLeft + elementWidth >
11848
- viewportWidth - viewportSpacing
12382
+ clampRightBound - marginWithContainer
11849
12383
  ) {
11850
- elementPositionLeft = viewportWidth - viewportSpacing - elementWidth;
12384
+ elementPositionLeft =
12385
+ clampRightBound - marginWithContainer - elementWidth;
11851
12386
  }
11852
12387
  }
11853
12388
 
11854
12389
  // Calculate vertical position (viewport-relative)
11855
12390
  let elementPositionTop;
11856
12391
  {
11857
- if (finalY === "above") {
11858
- // top is always anchorTop + insetTop - elementHeight - spacing — max-height truncates if needed.
11859
- const idealTop = anchorTop + insetTop - elementHeight - spacing;
12392
+ if (finalY === "top") {
12393
+ // top is always anchorTop + insetTop - elementHeight - marginWithAnchor — max-height truncates if needed.
12394
+ const idealTop = anchorTop + insetTop - elementHeight - marginWithAnchor;
11860
12395
  elementPositionTop =
11861
- idealTop < viewportSpacing ? viewportSpacing : idealTop;
11862
- } else if (finalY === "above-overlap") {
12396
+ idealTop < marginWithContainer ? marginWithContainer : idealTop;
12397
+ } else if (finalY === "inset-bottom") {
11863
12398
  const idealTop = anchorBottom - elementHeight;
11864
12399
  elementPositionTop =
11865
- idealTop < viewportSpacing ? viewportSpacing : idealTop;
12400
+ idealTop < marginWithContainer ? marginWithContainer : idealTop;
11866
12401
  } else if (finalY === "center") {
11867
12402
  elementPositionTop = anchorTop + anchorHeight / 2 - elementHeight / 2;
11868
- } else if (finalY === "below-overlap") {
12403
+ } else if (finalY === "inset-top") {
11869
12404
  const idealTop = anchorTop;
11870
12405
  elementPositionTop =
11871
12406
  idealTop % 1 === 0 ? idealTop : Math.floor(idealTop) + 1;
11872
12407
  } else {
11873
- // "below"
11874
- // top is always anchorBottom - insetBottom + spacing — max-height (via --space-available) truncates
12408
+ // "bottom"
12409
+ // top is always anchorBottom - insetBottom + marginWithAnchor — max-height (via --container-position-remaining-height) truncates
11875
12410
  // the element height so it doesn't overflow the viewport bottom.
11876
- const idealTop = anchorBottom - insetBottom + spacing;
12411
+ const idealTop = anchorBottom - insetBottom + marginWithAnchor;
11877
12412
  elementPositionTop =
11878
12413
  idealTop % 1 === 0 ? idealTop : Math.floor(idealTop) + 1;
11879
12414
  }
11880
- }
11881
-
11882
- // Persist resolved X/Y so subsequent calls start from here (avoids flickering).
11883
- // Fixed axes are not persisted.
11884
- if (!xIsFixed) {
11885
- element.setAttribute("data-position-x-current", finalX);
11886
- }
11887
- if (!yIsFixed) {
11888
- element.setAttribute("data-position-y-current", finalY);
11889
- }
11890
-
11891
- // Get document scroll for final coordinate conversion
11892
- const { scrollLeft, scrollTop } = document.documentElement;
12415
+ // Unlike the horizontal clamp above, there's normally no universal
12416
+ // vertical boundary clamp at all — "top"/"bottom" already clamp their
12417
+ // own idealTop inline, "inset-*"/"center" don't, and changing that
12418
+ // for every existing consumer (real-anchor "bottom" near the viewport
12419
+ // bottom relies on --container-position-remaining-height/max-height truncation instead of
12420
+ // repositioning) is out of scope here. Scoped strictly to the no-anchor
12421
+ // (container-docked) case, where it's new and safe: a container is
12422
+ // always meant to be respected on both axes.
12423
+ if (!hasValidAnchor) {
12424
+ if (elementPositionTop < availableTop + marginWithContainer) {
12425
+ elementPositionTop = availableTop + marginWithContainer;
12426
+ } else if (
12427
+ elementPositionTop + elementHeight >
12428
+ availableBottom - marginWithContainer
12429
+ ) {
12430
+ elementPositionTop =
12431
+ availableBottom - marginWithContainer - elementHeight;
12432
+ }
12433
+ }
12434
+ }
12435
+
12436
+ // Persist resolved X/Y so subsequent calls start from here (avoids
12437
+ // flickering) — and so CSS consumers (e.g. Popover's "clip" animation,
12438
+ // which reads data-position-y-current to pick which edge to reveal from)
12439
+ // can rely on it always reflecting the current side, fixed or not. A fixed
12440
+ // axis is never read back from this attribute (xIsFixed/yIsFixed always
12441
+ // wins over the stored value above), so persisting it here is purely for
12442
+ // those outside readers, not for this function's own flip logic.
12443
+ element.setAttribute("data-position-x-current", finalX);
12444
+ element.setAttribute("data-position-y-current", finalY);
12445
+
12446
+ // Convert the viewport-relative math above into whatever coordinate space
12447
+ // `element.style.top/left` actually needs. This is decided independently
12448
+ // of whether there's a real anchor: `element` might be `position:
12449
+ // absolute` relative to some container regardless (e.g. the custom
12450
+ // renderer in popover.jsx, which is always relative to its own
12451
+ // positioned ancestor whether or not it also has a real anchor) — that's
12452
+ // what `resolvedContainer` (explicit or auto-resolved above) communicates
12453
+ // even when `anchor` is also given. The container to convert into is
12454
+ // `resolvedContainer` when there's a real anchor, or (in the no-anchor
12455
+ // case) `effectiveAnchor` itself, since there the container *is* what's
12456
+ // being positioned against.
12457
+ const coordinateContainer = hasValidAnchor
12458
+ ? resolvedContainer
12459
+ : effectiveAnchor;
12460
+ let scrollLeft;
12461
+ let scrollTop;
12462
+ if (coordinateContainer && coordinateContainer !== document.documentElement) {
12463
+ // Reuse anchorRect/containerBorders when the coordinate container is
12464
+ // the same element already measured above (the no-anchor case);
12465
+ // otherwise (a real anchor positioned within a *different*, explicitly
12466
+ // given container) measure the container separately — the anchor's own
12467
+ // rect only matters for the positioning math above, not for this.
12468
+ const isSameAsEffectiveAnchor = coordinateContainer === effectiveAnchor;
12469
+ const coordinateRect = isSameAsEffectiveAnchor
12470
+ ? anchorRect
12471
+ : coordinateContainer.getBoundingClientRect();
12472
+ const coordinateBorders = isSameAsEffectiveAnchor
12473
+ ? containerBorders
12474
+ : getBorderSizes(coordinateContainer);
12475
+ scrollLeft =
12476
+ -coordinateRect.left -
12477
+ coordinateBorders.left +
12478
+ coordinateContainer.scrollLeft;
12479
+ scrollTop =
12480
+ -coordinateRect.top -
12481
+ coordinateBorders.top +
12482
+ coordinateContainer.scrollTop;
12483
+ } else {
12484
+ // No container to convert into (a plain real anchor, the common case
12485
+ // for Callout/Picker/Popover's own via-attribute renderer), or the
12486
+ // container is the viewport itself (Popover's via-attribute renderer
12487
+ // when docked, no real anchor) — either way, `element`'s own computed
12488
+ // `position` (fixed vs absolute, detected dynamically) decides whether
12489
+ // any scroll offset applies at all: none for position: fixed (already
12490
+ // viewport-relative — adding scroll would double-count it), the
12491
+ // document's own scroll for position: absolute (relative to the
12492
+ // initial containing block, i.e. document-relative) — including when
12493
+ // docked to the viewport, so the result lands at the visual center of
12494
+ // the viewport at its current scroll position.
12495
+ ({ scrollLeft, scrollTop } = getPositioningScrollOffset(element));
12496
+ }
12497
+ // visibleRectEffect recomputes this on every scroll tick, which is what
12498
+ // keeps it looking anchored as the page (or the container) scrolls
12499
+ // either way.
11893
12500
  const elementDocumentLeft = snapToPixel(elementPositionLeft + scrollLeft);
11894
12501
  const elementDocumentTop = snapToPixel(elementPositionTop + scrollTop);
11895
12502
  const anchorDocumentLeft = anchorLeft + scrollLeft;
@@ -11899,18 +12506,32 @@ const pickPositionRelativeTo = (
11899
12506
 
11900
12507
  // For overlap variants the element starts at the anchor edge (not past it),
11901
12508
  // so the usable space includes the anchor dimension.
11902
- // spacing (gap between anchor and element) and viewportSpacing are subtracted
12509
+ // marginWithAnchor (gap between anchor and element) and marginWithContainer are subtracted
11903
12510
  // so callers get the net usable space directly.
11904
12511
  const effectiveSpaceAbove =
11905
- (finalY === "above-overlap" ? spaceAbove + anchorHeight : spaceAbove) -
11906
- (finalY === "above" ? spacing : 0) -
11907
- viewportSpacing;
12512
+ (finalY === "inset-bottom" ? spaceAbove + anchorHeight : spaceAbove) -
12513
+ (finalY === "top" ? marginWithAnchor : 0) -
12514
+ marginWithContainer;
11908
12515
  const effectiveSpaceBelow =
11909
- (finalY === "below-overlap" ? spaceBelow + anchorHeight : spaceBelow) -
11910
- (finalY === "below" ? spacing : 0) -
11911
- viewportSpacing;
12516
+ (finalY === "inset-top" ? spaceBelow + anchorHeight : spaceBelow) -
12517
+ (finalY === "bottom" ? marginWithAnchor : 0) -
12518
+ marginWithContainer;
12519
+ const effectiveSpaceLeft =
12520
+ (finalX === "inset-right" ? spaceLeft + anchorWidth : spaceLeft) -
12521
+ (finalX === "left" ? marginWithAnchor : 0) -
12522
+ marginWithContainer;
12523
+ const effectiveSpaceRight =
12524
+ (finalX === "inset-left" ? spaceRight + anchorWidth : spaceRight) -
12525
+ (finalX === "right" ? marginWithAnchor : 0) -
12526
+ marginWithContainer;
11912
12527
 
11913
12528
  return {
12529
+ // Whether a real anchor actually ended up used — false when there's no
12530
+ // `anchor`, or it was rejected as too big.
12531
+ hasValidAnchor,
12532
+ // True only when `event` is a "resize" — see applyNewPosition's own
12533
+ // doc for why only resize-triggered repositions are meant to animate.
12534
+ shouldTransition: event?.type === "resize",
11914
12535
  positionX: finalX,
11915
12536
  positionY: finalY,
11916
12537
  left: elementDocumentLeft,
@@ -11921,13 +12542,72 @@ const pickPositionRelativeTo = (
11921
12542
  anchorTop: anchorDocumentTop,
11922
12543
  anchorRight: anchorDocumentRight,
11923
12544
  anchorBottom: anchorDocumentBottom,
11924
- spaceLeft: spaceLeft - viewportSpacing,
11925
- spaceRight: spaceRight - viewportSpacing,
12545
+ spaceLeft: effectiveSpaceLeft,
12546
+ spaceRight: effectiveSpaceRight,
11926
12547
  spaceAbove: effectiveSpaceAbove,
11927
12548
  spaceBelow: effectiveSpaceBelow,
11928
12549
  };
11929
12550
  };
11930
12551
 
12552
+ /**
12553
+ * Applies a `pickPositionRelativeTo` result to `element`. Drives
12554
+ * `--popup-position-transition-duration` (0s unless `shouldTransition`) so
12555
+ * a scroll-triggered reposition stays instant while a resize-triggered one
12556
+ * eases in — set via a CSS var rather than `transitionProperty` directly so
12557
+ * it doesn't clobber Popover/Dialog's own opacity/scale transition on the
12558
+ * same element; consumers declare `transition-duration:
12559
+ * var(--popup-position-transition-duration, 0s)` on `left`/`top` in CSS.
12560
+ */
12561
+ const applyNewPosition = (
12562
+ element,
12563
+ {
12564
+ left,
12565
+ top,
12566
+ shouldTransition,
12567
+ positionX,
12568
+ positionY,
12569
+ spaceLeft,
12570
+ spaceRight,
12571
+ spaceAbove,
12572
+ spaceBelow,
12573
+ },
12574
+ { transitionDuration = "0.25s" } = {},
12575
+ ) => {
12576
+ element.style.setProperty(
12577
+ "--popup-position-transition-duration",
12578
+ shouldTransition ? transitionDuration : "0s",
12579
+ );
12580
+ element.style.left = `${left}px`;
12581
+ element.style.top = `${top}px`;
12582
+
12583
+ if (positionY === "top" || positionY === "inset-bottom") {
12584
+ element.style.setProperty(
12585
+ "--container-position-remaining-height",
12586
+ `${spaceAbove}px`,
12587
+ );
12588
+ } else if (positionY === "bottom" || positionY === "inset-top") {
12589
+ element.style.setProperty(
12590
+ "--container-position-remaining-height",
12591
+ `${spaceBelow}px`,
12592
+ );
12593
+ } else {
12594
+ element.style.removeProperty("--container-position-remaining-height");
12595
+ }
12596
+ if (positionX === "left" || positionX === "inset-right") {
12597
+ element.style.setProperty(
12598
+ "--container-position-remaining-width",
12599
+ `${spaceLeft}px`,
12600
+ );
12601
+ } else if (positionX === "right" || positionX === "inset-left") {
12602
+ element.style.setProperty(
12603
+ "--container-position-remaining-width",
12604
+ `${spaceRight}px`,
12605
+ );
12606
+ } else {
12607
+ element.style.removeProperty("--container-position-remaining-width");
12608
+ }
12609
+ };
12610
+
11931
12611
  const [publishDebugger, subscribeDebugger] = createPubSub();
11932
12612
 
11933
12613
  const notifyDebuggerStart = () => {
@@ -15178,4 +15858,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
15178
15858
  };
15179
15859
  };
15180
15860
 
15181
- export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, captureScrollState, chainEvent, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findEvent, findFocusDelegateTarget, findFocusable, formatEventSideEffect, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getKeyboardEventDefaultAction, 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, measureLongestVisualLineWidth, measureScrollbar, measureWidestChildRow, mergeOneStyle, mergeTwoStyles, normalizeKeyboardKey, normalizeStyle, normalizeStyles, parseStyle, performTabNavigation, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, setAttribute, setAttributes, setStyles, snapToPixel, startDragToReorder, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
15861
+ export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, captureScrollState, chainEvent, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findEvent, findFocusDelegateTarget, findFocusable, formatEventSideEffect, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getKeyboardEventDefaultAction, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPositioningContainer, getPositioningScrollOffset, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isSameColor, isScrollable, measureLongestVisualLineWidth, measureScrollbar, measureWidestChildRow, mergeOneStyle, mergeTwoStyles, normalizeKeyboardKey, normalizeStyle, normalizeStyles, parsePositionArea, parseStyle, performTabNavigation, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, setAttribute, setAttributes, setStyles, snapToPixel, startDragToReorder, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, subscribeVisualViewportResizeSettled, subscribeWindowResizeSettled, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };