@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.
- package/dist/jsenv_dom.js +867 -187
- 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
|
|
5859
|
-
*
|
|
5860
|
-
*
|
|
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
|
-
|
|
6022
|
+
boundaryElement.addEventListener("mousedown", onmousedown, {
|
|
5941
6023
|
capture: true,
|
|
5942
6024
|
passive: false,
|
|
5943
6025
|
});
|
|
5944
6026
|
}
|
|
5945
|
-
|
|
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
|
-
|
|
6037
|
+
boundaryElement.removeEventListener("mousedown", onmousedown, {
|
|
5953
6038
|
capture: true,
|
|
5954
6039
|
passive: false,
|
|
5955
6040
|
});
|
|
5956
6041
|
}
|
|
5957
|
-
|
|
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
|
-
|
|
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
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
|
|
11307
|
-
|
|
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
|
-
*
|
|
11554
|
-
*
|
|
11555
|
-
*
|
|
11556
|
-
*
|
|
11557
|
-
* "
|
|
11558
|
-
* "
|
|
11559
|
-
*
|
|
11560
|
-
*
|
|
11561
|
-
* "
|
|
11562
|
-
*
|
|
11563
|
-
*
|
|
11564
|
-
* "
|
|
11565
|
-
* "
|
|
11566
|
-
*
|
|
11567
|
-
*
|
|
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
|
-
*
|
|
11941
|
+
* top ↔ bottom, inset-top ↔ inset-bottom, left ↔ right, inset-left ↔ inset-right
|
|
11570
11942
|
*
|
|
11571
|
-
*
|
|
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)
|
|
11576
|
-
*
|
|
11577
|
-
*
|
|
11578
|
-
*
|
|
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.
|
|
11581
|
-
*
|
|
11582
|
-
*
|
|
11583
|
-
*
|
|
11584
|
-
*
|
|
11585
|
-
*
|
|
11586
|
-
*
|
|
11587
|
-
*
|
|
11588
|
-
*
|
|
11589
|
-
*
|
|
11590
|
-
*
|
|
11591
|
-
*
|
|
11592
|
-
*
|
|
11593
|
-
*
|
|
11594
|
-
* @param {number} [options.
|
|
11595
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
|
|
11607
|
-
|
|
12012
|
+
positionArea = "bottom",
|
|
12013
|
+
positionAreaFixed,
|
|
12014
|
+
positionAreaWhenAnchorIsInvalid = "center",
|
|
12015
|
+
event,
|
|
12016
|
+
alignToContainerEdgeWhenAnchorNearEdge = 0,
|
|
11608
12017
|
minLeft = 0,
|
|
11609
|
-
|
|
12018
|
+
marginWithAnchor = 0,
|
|
11610
12019
|
alignToAnchorBox = "border-box",
|
|
11611
|
-
|
|
12020
|
+
marginWithContainer = 0,
|
|
12021
|
+
container,
|
|
11612
12022
|
} = {},
|
|
11613
12023
|
) => {
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
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
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
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
|
-
|
|
11631
|
-
const
|
|
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(
|
|
11647
|
-
const anchorPaddingSizes = getPaddingSizes(
|
|
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 =
|
|
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 =
|
|
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
|
-
"
|
|
11685
|
-
"
|
|
11686
|
-
"
|
|
11687
|
-
"
|
|
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 === "
|
|
11692
|
-
return spaceAbove -
|
|
12221
|
+
if (y === "top") {
|
|
12222
|
+
return spaceAbove - marginWithAnchor - marginWithContainer;
|
|
11693
12223
|
}
|
|
11694
|
-
if (y === "
|
|
11695
|
-
return spaceAbove + anchorHeight -
|
|
12224
|
+
if (y === "inset-bottom") {
|
|
12225
|
+
return spaceAbove + anchorHeight - marginWithContainer;
|
|
11696
12226
|
}
|
|
11697
|
-
if (y === "
|
|
11698
|
-
return spaceBelow -
|
|
12227
|
+
if (y === "bottom") {
|
|
12228
|
+
return spaceBelow - marginWithAnchor - marginWithContainer;
|
|
11699
12229
|
}
|
|
11700
|
-
if (y === "
|
|
11701
|
-
return spaceBelow + anchorHeight -
|
|
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
|
-
"
|
|
11746
|
-
"
|
|
11747
|
-
"left
|
|
11748
|
-
"right
|
|
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 === "
|
|
11753
|
-
return spaceLeft -
|
|
12282
|
+
if (x === "left") {
|
|
12283
|
+
return spaceLeft - marginWithAnchor - marginWithContainer;
|
|
11754
12284
|
}
|
|
11755
|
-
if (x === "left
|
|
11756
|
-
return
|
|
12285
|
+
if (x === "inset-left") {
|
|
12286
|
+
return availableRight - anchorLeft - marginWithContainer;
|
|
11757
12287
|
}
|
|
11758
|
-
if (x === "right
|
|
11759
|
-
return anchorRight -
|
|
12288
|
+
if (x === "inset-right") {
|
|
12289
|
+
return anchorRight - availableLeft - marginWithContainer;
|
|
11760
12290
|
}
|
|
11761
|
-
if (x === "
|
|
11762
|
-
return spaceRight -
|
|
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 === "
|
|
11799
|
-
elementPositionLeft =
|
|
11800
|
-
|
|
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
|
|
11804
|
-
const
|
|
11805
|
-
if (
|
|
11806
|
-
const anchorLeftIsVisible = effectiveAnchorLeft >=
|
|
11807
|
-
const anchorRightIsVisible = effectiveAnchorRight <=
|
|
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
|
|
11810
|
-
const distanceFromRightEdge =
|
|
12340
|
+
const availableCenter = availableLeft + availableWidth / 2;
|
|
12341
|
+
const distanceFromRightEdge = availableRight - effectiveAnchorRight;
|
|
11811
12342
|
elementPositionLeft =
|
|
11812
|
-
|
|
12343
|
+
availableCenter - distanceFromRightEdge / 2 - elementWidth / 2;
|
|
11813
12344
|
} else if (anchorLeftIsVisible && !anchorRightIsVisible) {
|
|
11814
|
-
const
|
|
11815
|
-
const distanceFromLeftEdge = -effectiveAnchorLeft;
|
|
12345
|
+
const availableCenter = availableLeft + availableWidth / 2;
|
|
12346
|
+
const distanceFromLeftEdge = availableLeft - effectiveAnchorLeft;
|
|
11816
12347
|
elementPositionLeft =
|
|
11817
|
-
|
|
12348
|
+
availableCenter - distanceFromLeftEdge / 2 - elementWidth / 2;
|
|
11818
12349
|
} else {
|
|
11819
|
-
elementPositionLeft =
|
|
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 (
|
|
12358
|
+
if (alignToContainerEdgeWhenAnchorNearEdge) {
|
|
11827
12359
|
const effectiveAnchorWidth =
|
|
11828
12360
|
effectiveAnchorRight - effectiveAnchorLeft;
|
|
11829
12361
|
const elementIsWiderThanAnchor = elementWidth > effectiveAnchorWidth;
|
|
11830
|
-
const
|
|
11831
|
-
effectiveAnchorLeft <
|
|
11832
|
-
|
|
11833
|
-
|
|
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
|
|
12370
|
+
} else if (finalX === "inset-right") {
|
|
11838
12371
|
elementPositionLeft = effectiveAnchorRight - elementWidth;
|
|
11839
12372
|
} else {
|
|
11840
|
-
// "
|
|
11841
|
-
elementPositionLeft = effectiveAnchorRight +
|
|
12373
|
+
// "right"
|
|
12374
|
+
elementPositionLeft = effectiveAnchorRight + marginWithAnchor;
|
|
11842
12375
|
}
|
|
11843
|
-
// Constrain horizontal position to
|
|
11844
|
-
|
|
11845
|
-
|
|
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
|
-
|
|
12382
|
+
clampRightBound - marginWithContainer
|
|
11849
12383
|
) {
|
|
11850
|
-
elementPositionLeft =
|
|
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 === "
|
|
11858
|
-
// top is always anchorTop + insetTop - elementHeight -
|
|
11859
|
-
const idealTop = anchorTop + insetTop - elementHeight -
|
|
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 <
|
|
11862
|
-
} else if (finalY === "
|
|
12396
|
+
idealTop < marginWithContainer ? marginWithContainer : idealTop;
|
|
12397
|
+
} else if (finalY === "inset-bottom") {
|
|
11863
12398
|
const idealTop = anchorBottom - elementHeight;
|
|
11864
12399
|
elementPositionTop =
|
|
11865
|
-
idealTop <
|
|
12400
|
+
idealTop < marginWithContainer ? marginWithContainer : idealTop;
|
|
11866
12401
|
} else if (finalY === "center") {
|
|
11867
12402
|
elementPositionTop = anchorTop + anchorHeight / 2 - elementHeight / 2;
|
|
11868
|
-
} else if (finalY === "
|
|
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
|
-
// "
|
|
11874
|
-
// top is always anchorBottom - insetBottom +
|
|
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 +
|
|
12411
|
+
const idealTop = anchorBottom - insetBottom + marginWithAnchor;
|
|
11877
12412
|
elementPositionTop =
|
|
11878
12413
|
idealTop % 1 === 0 ? idealTop : Math.floor(idealTop) + 1;
|
|
11879
12414
|
}
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
11891
|
-
|
|
11892
|
-
|
|
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
|
-
//
|
|
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 === "
|
|
11906
|
-
(finalY === "
|
|
11907
|
-
|
|
12512
|
+
(finalY === "inset-bottom" ? spaceAbove + anchorHeight : spaceAbove) -
|
|
12513
|
+
(finalY === "top" ? marginWithAnchor : 0) -
|
|
12514
|
+
marginWithContainer;
|
|
11908
12515
|
const effectiveSpaceBelow =
|
|
11909
|
-
(finalY === "
|
|
11910
|
-
(finalY === "
|
|
11911
|
-
|
|
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:
|
|
11925
|
-
spaceRight:
|
|
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 };
|