@jsenv/dom 0.15.0 → 0.16.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 +681 -140
- package/package.json +1 -1
package/dist/jsenv_dom.js
CHANGED
|
@@ -6323,7 +6323,10 @@ const getScrollContainer = (arg, { includeHidden } = {}) => {
|
|
|
6323
6323
|
}
|
|
6324
6324
|
return null;
|
|
6325
6325
|
}
|
|
6326
|
-
if (element.hasAttribute("popover")
|
|
6326
|
+
if (element.hasAttribute("popover")) {
|
|
6327
|
+
return getScrollingElement(element.ownerDocument);
|
|
6328
|
+
}
|
|
6329
|
+
if (element.tagName === "DIALOG" && element.matches(":modal")) {
|
|
6327
6330
|
return getScrollingElement(element.ownerDocument);
|
|
6328
6331
|
}
|
|
6329
6332
|
const position = getStyle(element, "position");
|
|
@@ -7363,6 +7366,74 @@ const applyWheelScrollThrough = (element, wheelEvent) => {
|
|
|
7363
7366
|
});
|
|
7364
7367
|
};
|
|
7365
7368
|
|
|
7369
|
+
/**
|
|
7370
|
+
* The element `element` is genuinely `position: absolute`/`fixed` relative
|
|
7371
|
+
* to: its own nearest positioned ancestor (walking up the DOM tree), or
|
|
7372
|
+
* `document.documentElement` (the viewport) if none is found.
|
|
7373
|
+
*
|
|
7374
|
+
* Also aware of `element` itself being promoted to the top layer: a
|
|
7375
|
+
* `<dialog>` actually shown modally (`showModal()`, matches `:modal` — a
|
|
7376
|
+
* `.show()`'d, non-modal dialog does NOT match and is positioned like any
|
|
7377
|
+
* other in-flow element instead, walked up normally below), or *any*
|
|
7378
|
+
* `[popover]` element, always uses the initial containing block (the
|
|
7379
|
+
* viewport) regardless of its own `position` or DOM ancestry — walking up
|
|
7380
|
+
* its own parent chain (what the rest of this function does) would give
|
|
7381
|
+
* the wrong answer for these two specifically, since their real DOM
|
|
7382
|
+
* position becomes irrelevant to their own containing block the moment
|
|
7383
|
+
* they're actually promoted. Checked via the `popover` attribute itself,
|
|
7384
|
+
* not the live `:popover-open` state — unlike `<dialog>`, a `[popover]`
|
|
7385
|
+
* element has no "local" mode: it's always top-layer-bound once shown,
|
|
7386
|
+
* regardless of whether it happens to be open right this moment, so the
|
|
7387
|
+
* static attribute alone is enough (and correct even when called just
|
|
7388
|
+
* before `showPopover()` actually runs, when `:popover-open` isn't true
|
|
7389
|
+
* yet).
|
|
7390
|
+
*
|
|
7391
|
+
* `document.documentElement` (not `document.body`, not `null`) is this
|
|
7392
|
+
* function's own "no real container — use the viewport" sentinel:
|
|
7393
|
+
* `documentElement` is the actual initial containing block, so the walk
|
|
7394
|
+
* below stops there without testing its own `position` (there's nothing
|
|
7395
|
+
* beyond it to fall back to anyway) — unlike the previous version of this
|
|
7396
|
+
* function, which stopped one level too early, at `document.body`, without
|
|
7397
|
+
* ever testing *its* `position` either (a `position: relative` body, for
|
|
7398
|
+
* instance, would have been silently skipped). Returning `documentElement`
|
|
7399
|
+
* instead of `null` also means no special-casing is needed by callers that
|
|
7400
|
+
* already compare a resolved container against `document.documentElement`
|
|
7401
|
+
* (see e.g. visible_rect.js's own `hasRealContainer` check).
|
|
7402
|
+
*/
|
|
7403
|
+
const getPositionedParent = (element) => {
|
|
7404
|
+
const isPromotedToTopLayer =
|
|
7405
|
+
(element.tagName === "DIALOG" && element.matches(":modal")) ||
|
|
7406
|
+
element.hasAttribute("popover");
|
|
7407
|
+
if (isPromotedToTopLayer) {
|
|
7408
|
+
return document.documentElement;
|
|
7409
|
+
}
|
|
7410
|
+
let parent = element.parentElement;
|
|
7411
|
+
while (parent && parent !== document.documentElement) {
|
|
7412
|
+
const position = window.getComputedStyle(parent).position;
|
|
7413
|
+
if (
|
|
7414
|
+
position === "relative" ||
|
|
7415
|
+
position === "absolute" ||
|
|
7416
|
+
position === "fixed"
|
|
7417
|
+
) {
|
|
7418
|
+
return parent;
|
|
7419
|
+
}
|
|
7420
|
+
parent = parent.parentElement;
|
|
7421
|
+
}
|
|
7422
|
+
return document.documentElement;
|
|
7423
|
+
};
|
|
7424
|
+
|
|
7425
|
+
/**
|
|
7426
|
+
* Walks `element` and its ancestors (stopping at, but not including,
|
|
7427
|
+
* `document.documentElement`) looking for the first one whose *computed*
|
|
7428
|
+
* `position` is `fixed` — i.e. pinned to the viewport, ignoring document
|
|
7429
|
+
* scroll, regardless of what `element` itself is positioned relative to.
|
|
7430
|
+
*
|
|
7431
|
+
* @param {Element} element
|
|
7432
|
+
* @returns {[left: number, top: number] | null} The fixed ancestor's own
|
|
7433
|
+
* viewport-relative `getBoundingClientRect()` origin, or `null` if neither
|
|
7434
|
+
* `element` nor any ancestor is fixed (i.e. `element` genuinely scrolls
|
|
7435
|
+
* with the document).
|
|
7436
|
+
*/
|
|
7366
7437
|
const findSelfOrAncestorFixedPosition = (element) => {
|
|
7367
7438
|
let current = element;
|
|
7368
7439
|
while (true) {
|
|
@@ -7460,14 +7531,18 @@ const createDragElementPositioner = (
|
|
|
7460
7531
|
let scrollableTop;
|
|
7461
7532
|
let convertScrollablePosition;
|
|
7462
7533
|
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7534
|
+
// getPositionedParent, not raw .offsetParent — offsetParent is null for a
|
|
7535
|
+
// position: fixed element, and also for one promoted to the top layer
|
|
7536
|
+
// (e.g. a <dialog>/[popover] being dragged by its own handle), which
|
|
7537
|
+
// crashes the fixed-position lookup below (findSelfOrAncestorFixedPosition
|
|
7538
|
+
// assumes a real starting element, not null). getPositionedParent never
|
|
7539
|
+
// returns null (document.documentElement instead — see its own doc).
|
|
7540
|
+
const positionedParent = getPositionedParent(elementToMove || element);
|
|
7466
7541
|
const scrollContainer = getScrollContainer(element);
|
|
7467
7542
|
const [getPositionOffsets, getScrollOffsets] = createGetOffsets({
|
|
7468
7543
|
positionedParent,
|
|
7469
7544
|
referencePositionedParent: referenceElement
|
|
7470
|
-
? referenceElement
|
|
7545
|
+
? getPositionedParent(referenceElement)
|
|
7471
7546
|
: positionedParent,
|
|
7472
7547
|
scrollContainer,
|
|
7473
7548
|
referenceScrollContainer: referenceElement
|
|
@@ -7721,7 +7796,7 @@ const isOverlayOf = (element, potentialTarget) => {
|
|
|
7721
7796
|
if (overlayTarget === potentialTarget) {
|
|
7722
7797
|
return true;
|
|
7723
7798
|
}
|
|
7724
|
-
const overlayTargetPositionedParent = overlayTarget
|
|
7799
|
+
const overlayTargetPositionedParent = getPositionedParent(overlayTarget);
|
|
7725
7800
|
if (overlayTargetPositionedParent === potentialTarget) {
|
|
7726
7801
|
return true;
|
|
7727
7802
|
}
|
|
@@ -10864,42 +10939,160 @@ const getResizeDirection = (element) => {
|
|
|
10864
10939
|
return { x, y };
|
|
10865
10940
|
};
|
|
10866
10941
|
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10942
|
+
// Shared by navi's own use_displayed_layout_effect.js (rich "navi_displayed"
|
|
10943
|
+
// CustomEvent, open transitions only) and visible_rect.js (needs both
|
|
10944
|
+
// directions: hide when a container closes, recheck when it reopens) — the
|
|
10945
|
+
// selector/open-detection/timing primitives are identical for both, only
|
|
10946
|
+
// what each does with a transition differs.
|
|
10947
|
+
const ANCESTOR_OPEN_SELECTOR = "dialog, details, [popover], [aria-expanded]";
|
|
10948
|
+
|
|
10949
|
+
const closestOpenableAncestor = (element) => {
|
|
10950
|
+
const parentElement = element.parentElement;
|
|
10951
|
+
if (!parentElement) {
|
|
10952
|
+
return null;
|
|
10953
|
+
}
|
|
10954
|
+
if (!parentElement.closest) {
|
|
10955
|
+
return null;
|
|
10879
10956
|
}
|
|
10880
|
-
return
|
|
10957
|
+
return parentElement.closest(ANCESTOR_OPEN_SELECTOR);
|
|
10958
|
+
};
|
|
10959
|
+
|
|
10960
|
+
const isAncestorOpen = (ancestor) => {
|
|
10961
|
+
if (ancestor.tagName === "DIALOG" || ancestor.hasAttribute("popover")) {
|
|
10962
|
+
return ancestor.matches(":popover-open, [open]");
|
|
10963
|
+
}
|
|
10964
|
+
if (ancestor.tagName === "DETAILS") {
|
|
10965
|
+
return ancestor.open;
|
|
10966
|
+
}
|
|
10967
|
+
if (ancestor.hasAttribute("aria-expanded")) {
|
|
10968
|
+
return ancestor.getAttribute("aria-expanded") === "true";
|
|
10969
|
+
}
|
|
10970
|
+
return true;
|
|
10971
|
+
};
|
|
10972
|
+
|
|
10973
|
+
const getAncestorOpenType = (ancestor) => {
|
|
10974
|
+
if (ancestor === document) {
|
|
10975
|
+
return "document";
|
|
10976
|
+
}
|
|
10977
|
+
if (ancestor.tagName === "DIALOG") {
|
|
10978
|
+
return "dialog";
|
|
10979
|
+
}
|
|
10980
|
+
if (ancestor.hasAttribute("popover")) {
|
|
10981
|
+
return "popover";
|
|
10982
|
+
}
|
|
10983
|
+
if (ancestor.tagName === "DETAILS") {
|
|
10984
|
+
return "details";
|
|
10985
|
+
}
|
|
10986
|
+
if (ancestor.hasAttribute("aria-expanded")) {
|
|
10987
|
+
return `${ancestor.tagName}[aria-expanded]`;
|
|
10988
|
+
}
|
|
10989
|
+
return `${ancestor.tagName}`;
|
|
10881
10990
|
};
|
|
10882
10991
|
|
|
10883
10992
|
/**
|
|
10884
|
-
*
|
|
10885
|
-
*
|
|
10886
|
-
*
|
|
10887
|
-
*
|
|
10888
|
-
*
|
|
10889
|
-
*
|
|
10890
|
-
*
|
|
10891
|
-
*
|
|
10892
|
-
*
|
|
10893
|
-
*
|
|
10894
|
-
*
|
|
10895
|
-
*
|
|
10896
|
-
*
|
|
10993
|
+
* Notifies `callback({ isOpen, ancestor, ancestorType, toggleEvent })` the
|
|
10994
|
+
* moment `ancestor`'s open state changes, in either direction — timed to
|
|
10995
|
+
* land strictly before the browser's next paint, so a caller reacting to it
|
|
10996
|
+
* (measurement, visibility tracking, layout) never flashes the stale state
|
|
10997
|
+
* first. Plain object, not a CustomEvent — there's no real DOM event behind
|
|
10998
|
+
* most of these transitions (see `toggleEvent` below), so wrapping the info
|
|
10999
|
+
* in one would mostly be manufacturing a fake event for no benefit.
|
|
11000
|
+
*
|
|
11001
|
+
* We deliberately do NOT use the native `toggle` event as the primary
|
|
11002
|
+
* signal, even though every <dialog>/<details>/[popover] fires one: per the
|
|
11003
|
+
* WHATWG spec it's dispatched via a *queued task* ("queue a popover toggle
|
|
11004
|
+
* event task"), not synchronously and not as a microtask. The element's
|
|
11005
|
+
* shown state itself (showPopover()/showModal()) still flips synchronously,
|
|
11006
|
+
* so the browser can — and does — paint it in its default, uncorrected
|
|
11007
|
+
* state before that queued task ever runs. Relying on `toggle` alone means
|
|
11008
|
+
* a reaction to it always arrives one paint late.
|
|
11009
|
+
*
|
|
11010
|
+
* Instead we watch `open`/`aria-expanded` via MutationObserver:
|
|
11011
|
+
* - <dialog>/<details> reflect `open` themselves, natively, synchronously.
|
|
11012
|
+
* - navi's own Popover.jsx sets `aria-expanded` synchronously in the same
|
|
11013
|
+
* call stack as showPopover() (see popover.jsx's own aria-expanded
|
|
11014
|
+
* comments) — not part of any web standard, just that library's own
|
|
11015
|
+
* convention, but reliable for anything built through it.
|
|
11016
|
+
* MutationObserver callbacks run as a microtask, strictly before paint —
|
|
11017
|
+
* exactly the timing needed, no ambiguity. `toggleEvent` is `undefined` on
|
|
11018
|
+
* this path (there's no native event to report — a mutation record isn't
|
|
11019
|
+
* one).
|
|
11020
|
+
*
|
|
11021
|
+
* The `toggle` listener is kept as a fallback, attached ONLY where the
|
|
11022
|
+
* MutationObserver above has no chance of ever firing: a bare [popover]
|
|
11023
|
+
* element with no `aria-expanded` of its own — i.e. one not built through
|
|
11024
|
+
* navi's own Popover.jsx (the only thing that reliably sets it). That's the
|
|
11025
|
+
* one case with no other synchronously-observable signal at all. It still
|
|
11026
|
+
* arrives a paint late, but a late correction beats none. `toggleEvent` is
|
|
11027
|
+
* the real `toggle` event on this path.
|
|
11028
|
+
*
|
|
11029
|
+
* @param {Element} ancestor
|
|
11030
|
+
* @param {(info: { isOpen: boolean, ancestor: Element, ancestorType: string, toggleEvent: Event | undefined }) => void} callback
|
|
11031
|
+
* @returns {() => void} cleanup — removes the observer/listener
|
|
10897
11032
|
*/
|
|
10898
|
-
const
|
|
10899
|
-
|
|
10900
|
-
|
|
11033
|
+
const observeAncestorOpenState = (ancestor, callback) => {
|
|
11034
|
+
const ancestorType = getAncestorOpenType(ancestor);
|
|
11035
|
+
const needsToggleFallback =
|
|
11036
|
+
ancestor.hasAttribute("popover") && !ancestor.hasAttribute("aria-expanded");
|
|
11037
|
+
if (needsToggleFallback) {
|
|
11038
|
+
const onToggle = (toggleEvent) => {
|
|
11039
|
+
callback({
|
|
11040
|
+
isOpen: isAncestorOpen(ancestor),
|
|
11041
|
+
ancestor,
|
|
11042
|
+
ancestorType,
|
|
11043
|
+
toggleEvent,
|
|
11044
|
+
});
|
|
11045
|
+
};
|
|
11046
|
+
ancestor.addEventListener("toggle", onToggle);
|
|
11047
|
+
return () => {
|
|
11048
|
+
ancestor.removeEventListener("toggle", onToggle);
|
|
11049
|
+
};
|
|
10901
11050
|
}
|
|
10902
|
-
|
|
11051
|
+
|
|
11052
|
+
// Edge-triggered on purpose: some consumers (e.g. Popover.jsx) set
|
|
11053
|
+
// aria-expanded both imperatively (in their own openEffect, for precise
|
|
11054
|
+
// ordering relative to forced reflows/transitions) AND declaratively via a
|
|
11055
|
+
// JSX prop derived from the same open state — the latter is a deliberate
|
|
11056
|
+
// "always reflect current truth" prop, but Preact diffs against its own
|
|
11057
|
+
// previous *rendered* value, not the live DOM, so any later re-render that
|
|
11058
|
+
// happens to occur while already open re-applies the same "true" value as
|
|
11059
|
+
// a genuinely new attribute mutation. Tracking wasOpen here collapses that
|
|
11060
|
+
// redundant open→open (or close→close) mutation instead of notifying
|
|
11061
|
+
// callback a second time for the same state.
|
|
11062
|
+
let wasOpen = isAncestorOpen(ancestor);
|
|
11063
|
+
const observer = new MutationObserver(() => {
|
|
11064
|
+
const isOpen = isAncestorOpen(ancestor);
|
|
11065
|
+
if (isOpen === wasOpen) {
|
|
11066
|
+
return;
|
|
11067
|
+
}
|
|
11068
|
+
wasOpen = isOpen;
|
|
11069
|
+
callback({
|
|
11070
|
+
isOpen,
|
|
11071
|
+
ancestor,
|
|
11072
|
+
ancestorType,
|
|
11073
|
+
toggleEvent: undefined,
|
|
11074
|
+
});
|
|
11075
|
+
});
|
|
11076
|
+
observer.observe(ancestor, {
|
|
11077
|
+
attributes: true,
|
|
11078
|
+
attributeFilter: ["open", "aria-expanded"],
|
|
11079
|
+
});
|
|
11080
|
+
return () => {
|
|
11081
|
+
observer.disconnect();
|
|
11082
|
+
};
|
|
11083
|
+
};
|
|
11084
|
+
|
|
11085
|
+
const onAncestorReopen = (el, callback) => {
|
|
11086
|
+
const nearestOpenableAncestor = closestOpenableAncestor(el);
|
|
11087
|
+
if (!nearestOpenableAncestor) {
|
|
11088
|
+
return () => {};
|
|
11089
|
+
}
|
|
11090
|
+
return observeAncestorOpenState(nearestOpenableAncestor, ({ isOpen }) => {
|
|
11091
|
+
if (!isOpen) {
|
|
11092
|
+
return;
|
|
11093
|
+
}
|
|
11094
|
+
callback();
|
|
11095
|
+
});
|
|
10903
11096
|
};
|
|
10904
11097
|
|
|
10905
11098
|
const getHeight = (element) => {
|
|
@@ -11296,10 +11489,10 @@ const MIN_CONTENT_VISIBILITY_RATIO = 0.6;
|
|
|
11296
11489
|
* Is 0 when ancestorClosed is true.
|
|
11297
11490
|
*
|
|
11298
11491
|
* @typedef {Object} VisibleRectInfo
|
|
11299
|
-
* @property {Event} event
|
|
11300
|
-
* @property {number} width
|
|
11301
|
-
* @property {number} height
|
|
11302
|
-
* @property {boolean} ancestorClosed
|
|
11492
|
+
* @property {Event} event - The DOM event (or CustomEvent) that triggered the check.
|
|
11493
|
+
* @property {number} width - Raw getBoundingClientRect() width of the element.
|
|
11494
|
+
* @property {number} height - Raw getBoundingClientRect() height of the element.
|
|
11495
|
+
* @property {boolean} ancestorClosed - True when a popover, dialog, or details ancestor is
|
|
11303
11496
|
* currently closed so the element is not rendered. All visibleRect values are 0 in that case.
|
|
11304
11497
|
* update() is called immediately on ancestor close and again (with false) on reopen.
|
|
11305
11498
|
*
|
|
@@ -11307,6 +11500,7 @@ const MIN_CONTENT_VISIBILITY_RATIO = 0.6;
|
|
|
11307
11500
|
* - Once synchronously on initialization (event.type = "initialization")
|
|
11308
11501
|
* - On document/container scroll, window resize, element resize, intersection changes, touch move
|
|
11309
11502
|
* - Immediately when an ancestor popover/dialog/details opens or closes
|
|
11503
|
+
* - Immediately when an ancestor popover/dialog starts or stops repositioning itself
|
|
11310
11504
|
*
|
|
11311
11505
|
* A bit like https://tetherjs.dev/ but different
|
|
11312
11506
|
*/
|
|
@@ -11332,8 +11526,72 @@ const visibleRectEffect = (
|
|
|
11332
11526
|
let lastMeasuredWidth;
|
|
11333
11527
|
let lastMeasuredHeight;
|
|
11334
11528
|
let ancestorClosedCount = 0;
|
|
11529
|
+
// Every ResizeObserver this effect owns (its own element-resize watcher
|
|
11530
|
+
// below, plus one per observeSize() call) unobserves itself the moment an
|
|
11531
|
+
// ancestor closes, reobserving once it reopens (see on_ancestor_events) —
|
|
11532
|
+
// closing a dialog/popover containing several watched elements can make
|
|
11533
|
+
// them all collapse to zero size in the same reflow, which is what trips
|
|
11534
|
+
// the browser's "ResizeObserver loop completed with undelivered
|
|
11535
|
+
// notifications" warning. Proactively unobserving avoids generating those
|
|
11536
|
+
// notifications instead of just reacting differently to them.
|
|
11537
|
+
// Set while an ancestor is itself mid-repositioning — on_ancestor_events'
|
|
11538
|
+
// own onNaviPositionTransition already drives this element's position
|
|
11539
|
+
// every frame for that duration, more accurately than the direct
|
|
11540
|
+
// window/visualViewport resize reaction below (on_resize) could. Gates
|
|
11541
|
+
// that reaction so it doesn't also fire mid-transition and animate its
|
|
11542
|
+
// own competing move toward a target computed from the anchor's still
|
|
11543
|
+
// mid-flight rect, racing the frame-by-frame follow loop.
|
|
11544
|
+
let ancestorRepositioningCount = 0;
|
|
11545
|
+
// check() runs on every scroll/resize/frame of an ancestor's own
|
|
11546
|
+
// transition, but plenty of those land on an identical result — skip
|
|
11547
|
+
// calling update() again when neither snapshot changed. Two snapshots,
|
|
11548
|
+
// not one, because pickPositionRelativeTo depends on both separately:
|
|
11549
|
+
// - lastVisibleRect: left/top/width/height, plus visibilityRatio (which
|
|
11550
|
+
// can change on its own — see its own ratio formula further down —
|
|
11551
|
+
// without any of the other four moving).
|
|
11552
|
+
// - lastViewportRect: not part of visibleRect at all, but an on-screen
|
|
11553
|
+
// keyboard opening/closing can shrink the viewport without moving
|
|
11554
|
+
// this element's own visibleRect by a single pixel, and
|
|
11555
|
+
// pickPositionRelativeTo's available space depends on it too.
|
|
11556
|
+
let lastVisibleRect = null;
|
|
11557
|
+
let lastViewportRect = null;
|
|
11558
|
+
let resizeWatchingPaused = false;
|
|
11559
|
+
const [publishResizeWatchingPausedChange, onResizeWatchingPausedChange] =
|
|
11560
|
+
createPubSub();
|
|
11561
|
+
const pauseResizeWatching = () => {
|
|
11562
|
+
if (resizeWatchingPaused) {
|
|
11563
|
+
return;
|
|
11564
|
+
}
|
|
11565
|
+
resizeWatchingPaused = true;
|
|
11566
|
+
publishResizeWatchingPausedChange(true);
|
|
11567
|
+
};
|
|
11568
|
+
const resumeResizeWatching = () => {
|
|
11569
|
+
if (!resizeWatchingPaused) {
|
|
11570
|
+
return;
|
|
11571
|
+
}
|
|
11572
|
+
resizeWatchingPaused = false;
|
|
11573
|
+
publishResizeWatchingPausedChange(false);
|
|
11574
|
+
};
|
|
11335
11575
|
const check = (event) => {
|
|
11336
11576
|
|
|
11577
|
+
// visualViewport, not window.innerWidth/Height: the layout viewport
|
|
11578
|
+
// doesn't shrink when the on-screen keyboard opens (same reasoning as
|
|
11579
|
+
// pickPositionRelativeTo's own identical choice). offsetLeft/Top matter
|
|
11580
|
+
// too, for pinch-zoom/pan. Computed here regardless of scroll container
|
|
11581
|
+
// (not just where the non-document branch below needs it) because a
|
|
11582
|
+
// keyboard opening can change pickPositionRelativeTo's available space
|
|
11583
|
+
// without moving this element's own visibleRect at all — see
|
|
11584
|
+
// viewportRectChanged further down.
|
|
11585
|
+
const visualViewport = window.visualViewport;
|
|
11586
|
+
const viewportWidth = visualViewport
|
|
11587
|
+
? visualViewport.width
|
|
11588
|
+
: window.innerWidth;
|
|
11589
|
+
const viewportHeight = visualViewport
|
|
11590
|
+
? visualViewport.height
|
|
11591
|
+
: window.innerHeight;
|
|
11592
|
+
const viewportOffsetLeft = visualViewport ? visualViewport.offsetLeft : 0;
|
|
11593
|
+
const viewportOffsetTop = visualViewport ? visualViewport.offsetTop : 0;
|
|
11594
|
+
|
|
11337
11595
|
// 1. Calculate element position relative to scrollable parent
|
|
11338
11596
|
const { scrollLeft, scrollTop } = scrollContainer;
|
|
11339
11597
|
const visibleAreaLeft = scrollLeft;
|
|
@@ -11434,22 +11692,26 @@ const visibleRectEffect = (
|
|
|
11434
11692
|
if (scrollContainerIsDocument) {
|
|
11435
11693
|
visibilityRatio = (widthVisible * heightVisible) / (width * height);
|
|
11436
11694
|
} else {
|
|
11437
|
-
// widthVisible/heightVisible are already clipped to the scroll
|
|
11438
|
-
// Now clip their viewport-relative counterparts against
|
|
11439
|
-
|
|
11440
|
-
|
|
11695
|
+
// widthVisible/heightVisible are already clipped to the scroll
|
|
11696
|
+
// container. Now clip their viewport-relative counterparts against
|
|
11697
|
+
// the viewport (viewportWidth/Height/OffsetLeft/OffsetTop computed
|
|
11698
|
+
// once, at the top of check() — see their own comment there).
|
|
11441
11699
|
// Container-clipped visible rect in viewport coordinates
|
|
11442
11700
|
const visibleLeft = overlayLeft;
|
|
11443
11701
|
const visibleTop = overlayTop;
|
|
11444
11702
|
const visibleRight = overlayLeft + widthVisible;
|
|
11445
11703
|
const visibleBottom = overlayTop + heightVisible;
|
|
11446
11704
|
// Intersect with viewport
|
|
11447
|
-
const clippedLeft =
|
|
11448
|
-
|
|
11705
|
+
const clippedLeft =
|
|
11706
|
+
visibleLeft < viewportOffsetLeft ? viewportOffsetLeft : visibleLeft;
|
|
11707
|
+
const clippedTop =
|
|
11708
|
+
visibleTop < viewportOffsetTop ? viewportOffsetTop : visibleTop;
|
|
11709
|
+
const viewportRight = viewportOffsetLeft + viewportWidth;
|
|
11710
|
+
const viewportBottom = viewportOffsetTop + viewportHeight;
|
|
11449
11711
|
const clippedRight =
|
|
11450
|
-
visibleRight >
|
|
11712
|
+
visibleRight > viewportRight ? viewportRight : visibleRight;
|
|
11451
11713
|
const clippedBottom =
|
|
11452
|
-
visibleBottom >
|
|
11714
|
+
visibleBottom > viewportBottom ? viewportBottom : visibleBottom;
|
|
11453
11715
|
const clippedWidth =
|
|
11454
11716
|
clippedRight > clippedLeft ? clippedRight - clippedLeft : 0;
|
|
11455
11717
|
const clippedHeight =
|
|
@@ -11466,12 +11728,49 @@ const visibleRectEffect = (
|
|
|
11466
11728
|
height: heightVisible,
|
|
11467
11729
|
visibilityRatio,
|
|
11468
11730
|
};
|
|
11469
|
-
|
|
11470
|
-
|
|
11471
|
-
|
|
11472
|
-
|
|
11473
|
-
|
|
11474
|
-
|
|
11731
|
+
// Not part of visibleRect itself, tracked only so viewportRectChanged
|
|
11732
|
+
// below can catch a keyboard opening/closing even when it doesn't move
|
|
11733
|
+
// this element's own visibleRect.
|
|
11734
|
+
const viewportRect = {
|
|
11735
|
+
viewportWidth,
|
|
11736
|
+
viewportHeight,
|
|
11737
|
+
viewportOffsetLeft,
|
|
11738
|
+
viewportOffsetTop,
|
|
11739
|
+
};
|
|
11740
|
+
const notify = (reason) => {
|
|
11741
|
+
update(visibleRect, {
|
|
11742
|
+
event,
|
|
11743
|
+
width,
|
|
11744
|
+
height,
|
|
11745
|
+
ancestorClosed: ancestorClosedCount > 0,
|
|
11746
|
+
});
|
|
11747
|
+
};
|
|
11748
|
+
|
|
11749
|
+
const visibleRectChanged =
|
|
11750
|
+
!lastVisibleRect ||
|
|
11751
|
+
lastVisibleRect.left !== visibleRect.left ||
|
|
11752
|
+
lastVisibleRect.top !== visibleRect.top ||
|
|
11753
|
+
lastVisibleRect.width !== visibleRect.width ||
|
|
11754
|
+
lastVisibleRect.height !== visibleRect.height ||
|
|
11755
|
+
lastVisibleRect.visibilityRatio !== visibleRect.visibilityRatio;
|
|
11756
|
+
if (visibleRectChanged) {
|
|
11757
|
+
lastVisibleRect = visibleRect;
|
|
11758
|
+
lastViewportRect = viewportRect;
|
|
11759
|
+
notify();
|
|
11760
|
+
return;
|
|
11761
|
+
}
|
|
11762
|
+
const viewportRectChanged =
|
|
11763
|
+
!lastViewportRect ||
|
|
11764
|
+
lastViewportRect.viewportWidth !== viewportRect.viewportWidth ||
|
|
11765
|
+
lastViewportRect.viewportHeight !== viewportRect.viewportHeight ||
|
|
11766
|
+
lastViewportRect.viewportOffsetLeft !== viewportRect.viewportOffsetLeft ||
|
|
11767
|
+
lastViewportRect.viewportOffsetTop !== viewportRect.viewportOffsetTop;
|
|
11768
|
+
if (viewportRectChanged) {
|
|
11769
|
+
lastVisibleRect = visibleRect;
|
|
11770
|
+
lastViewportRect = viewportRect;
|
|
11771
|
+
notify();
|
|
11772
|
+
return;
|
|
11773
|
+
}
|
|
11475
11774
|
};
|
|
11476
11775
|
|
|
11477
11776
|
check(initialEvent);
|
|
@@ -11549,8 +11848,24 @@ const visibleRectEffect = (
|
|
|
11549
11848
|
{
|
|
11550
11849
|
// See window_size.js's own module comment for why both of these go
|
|
11551
11850
|
// through their shared debounce instead of each keeping its own timer.
|
|
11552
|
-
|
|
11553
|
-
|
|
11851
|
+
const onWindowOrViewportResize = (event) => {
|
|
11852
|
+
// An ancestor's own navi_position_transition follow loop (see
|
|
11853
|
+
// on_ancestor_events below) is already re-checking this element's
|
|
11854
|
+
// position every frame, tracking the ancestor's live in-flight
|
|
11855
|
+
// position — more accurately than this debounced, ~100ms-after-the-
|
|
11856
|
+
// fact check could. Reacting here too would race it: a real
|
|
11857
|
+
// "resize" event makes shouldTransition true, so this would animate
|
|
11858
|
+
// its own competing move toward a target computed from the
|
|
11859
|
+
// anchor's current (still mid-flight) rect.
|
|
11860
|
+
if (ancestorRepositioningCount > 0) {
|
|
11861
|
+
return;
|
|
11862
|
+
}
|
|
11863
|
+
autoCheck(event);
|
|
11864
|
+
};
|
|
11865
|
+
addTeardown(
|
|
11866
|
+
subscribeVisualViewportResizeSettled(onWindowOrViewportResize),
|
|
11867
|
+
);
|
|
11868
|
+
addTeardown(subscribeWindowResizeSettled(onWindowOrViewportResize));
|
|
11554
11869
|
}
|
|
11555
11870
|
on_element_resize: {
|
|
11556
11871
|
if (skipElementResize) {
|
|
@@ -11585,16 +11900,30 @@ const visibleRectEffect = (
|
|
|
11585
11900
|
handlingResize = false;
|
|
11586
11901
|
});
|
|
11587
11902
|
resizeObserver.observe(element);
|
|
11903
|
+
const unsubscribeResizeWatchingPausedChange =
|
|
11904
|
+
onResizeWatchingPausedChange((paused) => {
|
|
11905
|
+
if (paused) {
|
|
11906
|
+
resizeObserver.unobserve(element);
|
|
11907
|
+
} else {
|
|
11908
|
+
resizeObserver.observe(element);
|
|
11909
|
+
}
|
|
11910
|
+
});
|
|
11588
11911
|
// Temporarily disconnect ResizeObserver to prevent feedback loops eventually caused by update function
|
|
11589
11912
|
onBeforeAutoCheck(() => {
|
|
11590
11913
|
resizeObserver.unobserve(element);
|
|
11591
11914
|
return () => {
|
|
11592
|
-
//
|
|
11593
|
-
//
|
|
11594
|
-
|
|
11915
|
+
// Not reobserved at all while an ancestor is closed (see
|
|
11916
|
+
// pauseResizeWatching/resumeResizeWatching above) — resumeResizeWatching's
|
|
11917
|
+
// own publish is what reobserves once it reopens instead.
|
|
11918
|
+
if (!resizeWatchingPaused) {
|
|
11919
|
+
// This triggers a new call to the resive observer that will be ignored thanks to
|
|
11920
|
+
// the widthDiff/heightDiff early return
|
|
11921
|
+
resizeObserver.observe(element);
|
|
11922
|
+
}
|
|
11595
11923
|
};
|
|
11596
11924
|
});
|
|
11597
11925
|
addTeardown(() => {
|
|
11926
|
+
unsubscribeResizeWatchingPausedChange();
|
|
11598
11927
|
resizeObserver.disconnect();
|
|
11599
11928
|
});
|
|
11600
11929
|
}
|
|
@@ -11648,29 +11977,27 @@ const visibleRectEffect = (
|
|
|
11648
11977
|
});
|
|
11649
11978
|
}
|
|
11650
11979
|
{
|
|
11651
|
-
let
|
|
11652
|
-
while (
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
ancestor.tagName === "DIALOG" || ancestor.tagName === "DETAILS"
|
|
11661
|
-
? !ancestor.open
|
|
11662
|
-
: !ancestor.matches(":popover-open");
|
|
11663
|
-
if (isInitiallyClosed) {
|
|
11664
|
-
ancestorClosedCount++;
|
|
11665
|
-
}
|
|
11980
|
+
let currentOpenableAncestor = closestOpenableAncestor(element);
|
|
11981
|
+
while (currentOpenableAncestor) {
|
|
11982
|
+
const openableAncestor = currentOpenableAncestor;
|
|
11983
|
+
if (!isAncestorOpen(openableAncestor)) {
|
|
11984
|
+
ancestorClosedCount++;
|
|
11985
|
+
pauseResizeWatching();
|
|
11986
|
+
}
|
|
11987
|
+
const removeOpenStateObserver = observeAncestorOpenState(
|
|
11988
|
+
openableAncestor,
|
|
11666
11989
|
// eslint-disable-next-line no-loop-func
|
|
11667
|
-
|
|
11668
|
-
|
|
11669
|
-
ancestor.tagName === "DETAILS"
|
|
11670
|
-
? !ancestor.open
|
|
11671
|
-
: e.newState === "closed";
|
|
11672
|
-
if (isClosed) {
|
|
11990
|
+
({ isOpen, toggleEvent }) => {
|
|
11991
|
+
if (!isOpen) {
|
|
11673
11992
|
ancestorClosedCount++;
|
|
11993
|
+
pauseResizeWatching();
|
|
11994
|
+
// Invalidates check()'s own "did anything actually change"
|
|
11995
|
+
// caches — without this, reopening onto the exact same
|
|
11996
|
+
// geometry/viewport as before closing would look unchanged to
|
|
11997
|
+
// check() and it would skip calling update() again, leaving a
|
|
11998
|
+
// consumer stuck showing this closed/zeroed state.
|
|
11999
|
+
lastVisibleRect = null;
|
|
12000
|
+
lastViewportRect = null;
|
|
11674
12001
|
update(
|
|
11675
12002
|
{
|
|
11676
12003
|
left: 0,
|
|
@@ -11681,35 +12008,91 @@ const visibleRectEffect = (
|
|
|
11681
12008
|
height: 0,
|
|
11682
12009
|
visibilityRatio: 0,
|
|
11683
12010
|
},
|
|
11684
|
-
{
|
|
12011
|
+
{
|
|
12012
|
+
event: toggleEvent ?? new CustomEvent("ancestor_close"),
|
|
12013
|
+
width: 0,
|
|
12014
|
+
height: 0,
|
|
12015
|
+
ancestorClosed: true,
|
|
12016
|
+
},
|
|
11685
12017
|
);
|
|
11686
|
-
|
|
11687
|
-
if (ancestorClosedCount > 0) {
|
|
11688
|
-
ancestorClosedCount--;
|
|
11689
|
-
}
|
|
11690
|
-
if (ancestorClosedCount === 0) {
|
|
11691
|
-
check(e);
|
|
11692
|
-
}
|
|
12018
|
+
return;
|
|
11693
12019
|
}
|
|
11694
|
-
|
|
11695
|
-
|
|
12020
|
+
if (ancestorClosedCount > 0) {
|
|
12021
|
+
ancestorClosedCount--;
|
|
12022
|
+
}
|
|
12023
|
+
if (ancestorClosedCount === 0) {
|
|
12024
|
+
resumeResizeWatching();
|
|
12025
|
+
check(toggleEvent ?? new CustomEvent("ancestor_open"));
|
|
12026
|
+
}
|
|
12027
|
+
},
|
|
12028
|
+
);
|
|
11696
12029
|
|
|
11697
|
-
|
|
12030
|
+
const onNaviPositionChange = (e) => {
|
|
12031
|
+
autoCheck(e);
|
|
12032
|
+
};
|
|
12033
|
+
openableAncestor.addEventListener(
|
|
12034
|
+
"navi_position_change",
|
|
12035
|
+
onNaviPositionChange,
|
|
12036
|
+
);
|
|
12037
|
+
// Dispatched by applyNewPosition's own notifyPositionTransition
|
|
12038
|
+
// around this ancestor's own left/top animation (distinct from
|
|
12039
|
+
// navi_position_change, fired once with the final target, not per
|
|
12040
|
+
// frame). The anchor this element is positioned against may live
|
|
12041
|
+
// inside that ancestor and be moving right now — rather than hiding
|
|
12042
|
+
// for the duration (an opacity flicker once it settles reads worse
|
|
12043
|
+
// than a slightly-behind position), autoCheck() every frame for as
|
|
12044
|
+
// long as the animation runs, so this element stays in lockstep.
|
|
12045
|
+
// autoCheck, not check directly, so this element's own
|
|
12046
|
+
// ResizeObserver(s) stay unobserved for the loop's duration too —
|
|
12047
|
+
// repositioning every frame can itself cause reflows. e.detail.onEnd
|
|
12048
|
+
// stops the loop and settles on one final check once it ends.
|
|
12049
|
+
let positionTransitionRafId = null;
|
|
12050
|
+
let isTrackingPositionTransition = false;
|
|
12051
|
+
// ancestorRepositioningCount is intentionally shared across every
|
|
12052
|
+
// ancestor level (declared once, outside this loop) — it's a
|
|
12053
|
+
// single "is this element's position currently being driven by
|
|
12054
|
+
// some ancestor's transition" flag for the element itself, not
|
|
12055
|
+
// per-ancestor state.
|
|
12056
|
+
// eslint-disable-next-line no-loop-func
|
|
12057
|
+
const onNaviPositionTransition = (e) => {
|
|
12058
|
+
cancelAnimationFrame(positionTransitionRafId);
|
|
12059
|
+
if (!isTrackingPositionTransition) {
|
|
12060
|
+
isTrackingPositionTransition = true;
|
|
12061
|
+
ancestorRepositioningCount++;
|
|
12062
|
+
}
|
|
12063
|
+
const loop = () => {
|
|
11698
12064
|
autoCheck(e);
|
|
12065
|
+
positionTransitionRafId = requestAnimationFrame(loop);
|
|
11699
12066
|
};
|
|
11700
|
-
|
|
12067
|
+
loop();
|
|
12068
|
+
e.detail.onEnd(() => {
|
|
12069
|
+
cancelAnimationFrame(positionTransitionRafId);
|
|
12070
|
+
if (isTrackingPositionTransition) {
|
|
12071
|
+
isTrackingPositionTransition = false;
|
|
12072
|
+
ancestorRepositioningCount--;
|
|
12073
|
+
}
|
|
12074
|
+
autoCheck(e);
|
|
12075
|
+
});
|
|
12076
|
+
};
|
|
12077
|
+
openableAncestor.addEventListener(
|
|
12078
|
+
"navi_position_transition",
|
|
12079
|
+
onNaviPositionTransition,
|
|
12080
|
+
);
|
|
12081
|
+
addTeardown(() => {
|
|
12082
|
+
removeOpenStateObserver();
|
|
12083
|
+
cancelAnimationFrame(positionTransitionRafId);
|
|
12084
|
+
openableAncestor.removeEventListener(
|
|
11701
12085
|
"navi_position_change",
|
|
11702
12086
|
onNaviPositionChange,
|
|
11703
12087
|
);
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
|
|
11708
|
-
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
current = current.parentElement;
|
|
12088
|
+
openableAncestor.removeEventListener(
|
|
12089
|
+
"navi_position_transition",
|
|
12090
|
+
onNaviPositionTransition,
|
|
12091
|
+
);
|
|
12092
|
+
});
|
|
12093
|
+
currentOpenableAncestor = closestOpenableAncestor(
|
|
12094
|
+
currentOpenableAncestor,
|
|
12095
|
+
);
|
|
11713
12096
|
}
|
|
11714
12097
|
}
|
|
11715
12098
|
}
|
|
@@ -11774,20 +12157,45 @@ const visibleRectEffect = (
|
|
|
11774
12157
|
});
|
|
11775
12158
|
});
|
|
11776
12159
|
resizeObserver.observe(elementToObserve);
|
|
12160
|
+
// An ancestor may already be closed by the time a consumer calls
|
|
12161
|
+
// observeSize (e.g. Callout's own observeSize(calloutMessageElement)
|
|
12162
|
+
// call happens after visibleRectEffect itself returns) — keep this new
|
|
12163
|
+
// observer consistent with that already-paused state instead of
|
|
12164
|
+
// observing it only to immediately generate a closed-container
|
|
12165
|
+
// notification.
|
|
12166
|
+
if (resizeWatchingPaused) {
|
|
12167
|
+
resizeObserver.unobserve(elementToObserve);
|
|
12168
|
+
}
|
|
12169
|
+
const unsubscribeResizeWatchingPausedChange = onResizeWatchingPausedChange(
|
|
12170
|
+
(paused) => {
|
|
12171
|
+
if (paused) {
|
|
12172
|
+
resizeObserver.unobserve(elementToObserve);
|
|
12173
|
+
} else {
|
|
12174
|
+
resizeObserver.observe(elementToObserve);
|
|
12175
|
+
}
|
|
12176
|
+
},
|
|
12177
|
+
);
|
|
11777
12178
|
const cleanupAutoCheck = onBeforeAutoCheck(() => {
|
|
11778
12179
|
resizeObserver.unobserve(elementToObserve);
|
|
11779
12180
|
return () => {
|
|
11780
|
-
|
|
12181
|
+
// Not reobserved at all while an ancestor is closed (see
|
|
12182
|
+
// pauseResizeWatching/resumeResizeWatching) — resumeResizeWatching's
|
|
12183
|
+
// own publish is what reobserves once it reopens instead.
|
|
12184
|
+
if (!resizeWatchingPaused) {
|
|
12185
|
+
resizeObserver.observe(elementToObserve);
|
|
12186
|
+
}
|
|
11781
12187
|
};
|
|
11782
12188
|
});
|
|
11783
12189
|
addTeardown(() => {
|
|
11784
12190
|
if (pendingFrame !== null) {
|
|
11785
12191
|
cancelAnimationFrame(pendingFrame);
|
|
11786
12192
|
}
|
|
12193
|
+
unsubscribeResizeWatchingPausedChange();
|
|
11787
12194
|
resizeObserver.disconnect();
|
|
11788
12195
|
});
|
|
11789
12196
|
return () => {
|
|
11790
12197
|
cleanupAutoCheck();
|
|
12198
|
+
unsubscribeResizeWatchingPausedChange();
|
|
11791
12199
|
if (pendingFrame !== null) {
|
|
11792
12200
|
cancelAnimationFrame(pendingFrame);
|
|
11793
12201
|
}
|
|
@@ -11984,17 +12392,18 @@ const toContainerAlignedPosition = (value) => {
|
|
|
11984
12392
|
* there's a real `anchor`, since `element` can be container-relative either way (e.g. the
|
|
11985
12393
|
* custom renderer in popover.jsx, always relative to its own positioned ancestor whether
|
|
11986
12394
|
* or not it also has a real anchor). Whenever not explicitly given, this is always
|
|
11987
|
-
* resolved automatically via `
|
|
12395
|
+
* resolved automatically via `getPositionedParent(element)` instead — regardless of
|
|
11988
12396
|
* `hasValidAnchor` — so a caller that never thinks about `container` at all still gets the
|
|
11989
|
-
* right behavior on its own: `
|
|
11990
|
-
* `
|
|
11991
|
-
*
|
|
11992
|
-
*
|
|
11993
|
-
*
|
|
11994
|
-
*
|
|
11995
|
-
*
|
|
11996
|
-
*
|
|
11997
|
-
*
|
|
12397
|
+
* right behavior on its own: `document.documentElement` from `getPositionedParent` (an
|
|
12398
|
+
* `element` promoted to the top layer — a `[popover]` while shown, or a `<dialog>` while
|
|
12399
|
+
* actually modal — or one with no positioned ancestor at all, e.g. Callout's own element)
|
|
12400
|
+
* falls back to the traditional document-relative path below, exactly as if `container`
|
|
12401
|
+
* genuinely didn't apply; anything else `getPositionedParent` finds (a real positioned
|
|
12402
|
+
* ancestor) is used the same way an explicit `container` would be. A container that
|
|
12403
|
+
* resolves to `document.documentElement` (the viewport) produces identical output to the
|
|
12404
|
+
* plain document-relative path either way, since the document's own scroll and the
|
|
12405
|
+
* viewport's own origin already coincide with what this generically computes for any other
|
|
12406
|
+
* container element. When there's a real container (explicit or resolved) either way: the final
|
|
11998
12407
|
* `left`/`top` (and the returned `anchorLeft/Top/Right/Bottom`) are expressed relative to
|
|
11999
12408
|
* its own padding-box origin plus its own scroll, instead of the document's — `element`'s
|
|
12000
12409
|
* own computed `position` is *not* consulted in that case, unlike the traditional path.
|
|
@@ -12040,9 +12449,11 @@ const pickPositionRelativeTo = (
|
|
|
12040
12449
|
// never gets offered more room (anchor-too-big check, flip decisions,
|
|
12041
12450
|
// clamp) than its own container — resolvedContainer's own padding-box
|
|
12042
12451
|
// edges when there is one — actually has.
|
|
12043
|
-
|
|
12044
|
-
|
|
12045
|
-
|
|
12452
|
+
// Always a real element now (never null/undefined) — getPositionedParent
|
|
12453
|
+
// itself never returns anything falsy, document.documentElement (the
|
|
12454
|
+
// viewport) included.
|
|
12455
|
+
const resolvedContainer = container ?? getPositionedParent(element);
|
|
12456
|
+
const hasRealContainer = resolvedContainer !== document.documentElement;
|
|
12046
12457
|
const containerRect = hasRealContainer
|
|
12047
12458
|
? resolvedContainer.getBoundingClientRect()
|
|
12048
12459
|
: null;
|
|
@@ -12118,14 +12529,13 @@ const pickPositionRelativeTo = (
|
|
|
12118
12529
|
positionXFixed = positionX;
|
|
12119
12530
|
positionYFixed = positionY;
|
|
12120
12531
|
}
|
|
12121
|
-
// resolvedContainer was already resolved above.
|
|
12122
|
-
//
|
|
12123
|
-
//
|
|
12124
|
-
//
|
|
12125
|
-
//
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
: resolvedContainer || document.documentElement;
|
|
12532
|
+
// resolvedContainer was already resolved above. document.documentElement
|
|
12533
|
+
// from getPositionedParent (a popover/dialog element, e.g. Callout's own,
|
|
12534
|
+
// or one with no positioned ancestor at all) falls through to the
|
|
12535
|
+
// traditional document-relative path below all the same, so an existing
|
|
12536
|
+
// caller that never thinks about `container` at all keeps behaving
|
|
12537
|
+
// exactly as before.
|
|
12538
|
+
const effectiveAnchor = hasValidAnchor ? anchor : resolvedContainer;
|
|
12129
12539
|
// document.documentElement is used as a sentinel "the viewport" value: an
|
|
12130
12540
|
// anchorless popup should center/place itself against the visual
|
|
12131
12541
|
// viewport, not against <html>'s own box — which, unlike the viewport,
|
|
@@ -12317,7 +12727,14 @@ const pickPositionRelativeTo = (
|
|
|
12317
12727
|
if (currentFitsEnough) {
|
|
12318
12728
|
finalX = activeX;
|
|
12319
12729
|
} else {
|
|
12320
|
-
|
|
12730
|
+
// Only flip if the opposite side has more space — avoids oscillation
|
|
12731
|
+
// when neither side has enough room (both fail the ratio). Mirrors
|
|
12732
|
+
// the Y-axis branch above; missing here was the actual cause of a
|
|
12733
|
+
// real left/right flicker on a narrow viewport (neither side ever
|
|
12734
|
+
// "fits enough", so this branch ran on every reposition).
|
|
12735
|
+
const opposite = oppositeX[activeX];
|
|
12736
|
+
const oppositeHasMoreSpace = spaceFor(opposite) > spaceFor(activeX);
|
|
12737
|
+
finalX = oppositeHasMoreSpace ? opposite : activeX;
|
|
12321
12738
|
}
|
|
12322
12739
|
}
|
|
12323
12740
|
}
|
|
@@ -12549,14 +12966,110 @@ const pickPositionRelativeTo = (
|
|
|
12549
12966
|
};
|
|
12550
12967
|
};
|
|
12551
12968
|
|
|
12969
|
+
// Per-element bookkeeping for the currently in-flight, self-driven position
|
|
12970
|
+
// transition, if any — see notifyPositionTransition's own doc for why this
|
|
12971
|
+
// is animation-driven rather than listening for the browser's own
|
|
12972
|
+
// transitionrun/transitionend: element -> { animation, endCallbacks }.
|
|
12973
|
+
const pendingPositionTransitions = new WeakMap();
|
|
12974
|
+
|
|
12975
|
+
// Reads `cssVarName` off `element` (getComputedStyle, so it's whatever the
|
|
12976
|
+
// cascade resolves to — a consumer can set it inline, in its own CSS rule,
|
|
12977
|
+
// or not at all) and converts it to milliseconds: "0.25s" -> 250, "250ms" ->
|
|
12978
|
+
// 250. Falls back to `fallbackMs` when unset/empty/unparsable, so a caller
|
|
12979
|
+
// never has to declare the CSS var itself just to get a sane default
|
|
12980
|
+
// duration — it only needs to when it actually wants to override it.
|
|
12981
|
+
const parseTransitionDurationMs = (element, cssVarName, fallbackMs) => {
|
|
12982
|
+
const trimmed = getStyle(element, cssVarName).trim();
|
|
12983
|
+
if (!trimmed) {
|
|
12984
|
+
return fallbackMs;
|
|
12985
|
+
}
|
|
12986
|
+
if (trimmed.endsWith("ms")) {
|
|
12987
|
+
return parseFloat(trimmed);
|
|
12988
|
+
}
|
|
12989
|
+
if (trimmed.endsWith("s")) {
|
|
12990
|
+
return parseFloat(trimmed) * 1000;
|
|
12991
|
+
}
|
|
12992
|
+
const parsed = parseFloat(trimmed);
|
|
12993
|
+
return Number.isNaN(parsed) ? fallbackMs : parsed;
|
|
12994
|
+
};
|
|
12995
|
+
|
|
12552
12996
|
/**
|
|
12553
|
-
*
|
|
12554
|
-
*
|
|
12555
|
-
*
|
|
12556
|
-
*
|
|
12557
|
-
*
|
|
12558
|
-
*
|
|
12559
|
-
*
|
|
12997
|
+
* Dispatches a single "navi_position_transition" event on `element`,
|
|
12998
|
+
* self-driven rather than confirmed by the browser's own `transitionrun` —
|
|
12999
|
+
* `applyNewPosition` calls this exactly when it knows it just started a
|
|
13000
|
+
* left/top `animation`, so there's nothing to wait for. transitionrun was
|
|
13001
|
+
* tried first and dropped: it reacts to *any* transition sharing the
|
|
13002
|
+
* element (a scale/opacity entrance would wrongly hide a descendant too),
|
|
13003
|
+
* and filtering by `propertyName` is unreliable (observed firing for "top"
|
|
13004
|
+
* instead of "left" in practice, despite the transition-property order).
|
|
13005
|
+
* A dedicated `Animation` sidesteps both.
|
|
13006
|
+
*
|
|
13007
|
+
* A descendant anchored inside `element` (see on_ancestor_events)
|
|
13008
|
+
* re-checks its own position every frame for as long as this animation
|
|
13009
|
+
* runs, instead of showing a stale position. `event.detail.onEnd(callback)`
|
|
13010
|
+
* is how it learns when the animation actually ends.
|
|
13011
|
+
*
|
|
13012
|
+
* A second reposition landing mid-animation cancels the pending one and
|
|
13013
|
+
* flushes its own registered callbacks immediately (same spirit as a real
|
|
13014
|
+
* `transitioncancel`), so nothing is left waiting on a superseded `onEnd`.
|
|
13015
|
+
*
|
|
13016
|
+
* `commitStyles()` below isn't what makes the final position correct —
|
|
13017
|
+
* `applyNewPosition` already sets the specified `left`/`top` before this
|
|
13018
|
+
* animation starts, so it takes back over once the active duration elapses
|
|
13019
|
+
* regardless. It just makes that explicit instead of relying on `fill:
|
|
13020
|
+
* "none"` timing, and drops the finished Animation instead of leaving it.
|
|
13021
|
+
*/
|
|
13022
|
+
const notifyPositionTransition = (element, animation) => {
|
|
13023
|
+
const pending = pendingPositionTransitions.get(element);
|
|
13024
|
+
if (pending) {
|
|
13025
|
+
pending.animation.cancel();
|
|
13026
|
+
for (const callback of pending.endCallbacks) {
|
|
13027
|
+
callback();
|
|
13028
|
+
}
|
|
13029
|
+
}
|
|
13030
|
+
const endCallbacks = [];
|
|
13031
|
+
dispatchCustomEvent(element, "navi_position_transition", {
|
|
13032
|
+
onEnd: (callback) => {
|
|
13033
|
+
endCallbacks.push(callback);
|
|
13034
|
+
},
|
|
13035
|
+
});
|
|
13036
|
+
const current = { animation, endCallbacks };
|
|
13037
|
+
pendingPositionTransitions.set(element, current);
|
|
13038
|
+
animation.finished
|
|
13039
|
+
.then(() => {
|
|
13040
|
+
if (pendingPositionTransitions.get(element) === current) {
|
|
13041
|
+
pendingPositionTransitions.delete(element);
|
|
13042
|
+
}
|
|
13043
|
+
try {
|
|
13044
|
+
animation.commitStyles();
|
|
13045
|
+
} catch {
|
|
13046
|
+
// Element no longer rendered (removed/hidden mid-animation) —
|
|
13047
|
+
// nothing to commit to, and left/top were already final anyway.
|
|
13048
|
+
}
|
|
13049
|
+
animation.cancel();
|
|
13050
|
+
for (const callback of endCallbacks) {
|
|
13051
|
+
callback();
|
|
13052
|
+
}
|
|
13053
|
+
})
|
|
13054
|
+
.catch(() => {
|
|
13055
|
+
// Cancelled by a subsequent reposition — already flushed above.
|
|
13056
|
+
});
|
|
13057
|
+
};
|
|
13058
|
+
|
|
13059
|
+
/**
|
|
13060
|
+
* Applies a `pickPositionRelativeTo` result to `element`. `left`/`top` are
|
|
13061
|
+
* set instantly (a scroll-triggered reposition should never lag its
|
|
13062
|
+
* target); when `shouldTransition` is set (a resize-triggered reposition),
|
|
13063
|
+
* the visual move is played out via `element.animate()` instead — kept
|
|
13064
|
+
* independent of Popover/Dialog/Callout's own opacity/scale/display CSS
|
|
13065
|
+
* transition on the same element, so neither can clobber the other (see
|
|
13066
|
+
* notifyPositionTransition's own doc for why a dedicated Animation over a
|
|
13067
|
+
* CSS one). Duration comes from `--popup-position-transition-duration`
|
|
13068
|
+
* (parseTransitionDurationMs), falling back to 180ms unset.
|
|
13069
|
+
* Dispatches navi_position_transition when it starts such an animation, and
|
|
13070
|
+
* navi_position_change unconditionally — every caller (Dialog, Popover,
|
|
13071
|
+
* Callout) wants both, so a descendant anchored inside `element` can always
|
|
13072
|
+
* recheck its own position whenever `element` moves.
|
|
12560
13073
|
*/
|
|
12561
13074
|
const applyNewPosition = (
|
|
12562
13075
|
element,
|
|
@@ -12571,15 +13084,7 @@ const applyNewPosition = (
|
|
|
12571
13084
|
spaceAbove,
|
|
12572
13085
|
spaceBelow,
|
|
12573
13086
|
},
|
|
12574
|
-
{ transitionDuration = "0.25s" } = {},
|
|
12575
13087
|
) => {
|
|
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
13088
|
if (positionY === "top" || positionY === "inset-bottom") {
|
|
12584
13089
|
element.style.setProperty(
|
|
12585
13090
|
"--container-position-remaining-height",
|
|
@@ -12606,6 +13111,42 @@ const applyNewPosition = (
|
|
|
12606
13111
|
} else {
|
|
12607
13112
|
element.style.removeProperty("--container-position-remaining-width");
|
|
12608
13113
|
}
|
|
13114
|
+
|
|
13115
|
+
// A single implicit keyframe turned out not to work here: the WAAPI
|
|
13116
|
+
// "neutral" start keyframe isn't frozen at `animate()` call time, it's
|
|
13117
|
+
// resolved from the underlying value when the animation is first
|
|
13118
|
+
// *sampled* (the next frame) — by then `element.style.left`/`top` below
|
|
13119
|
+
// has already been overwritten with the new target, so start === end and
|
|
13120
|
+
// nothing visibly moves (observed as the dialog just jumping). Reading
|
|
13121
|
+
// the previous value ourselves, before overwriting it, and passing both
|
|
13122
|
+
// keyframes explicitly sidesteps that entirely.
|
|
13123
|
+
const previousLeft = parseFloat(element.style.left) || left;
|
|
13124
|
+
const previousTop = parseFloat(element.style.top) || top;
|
|
13125
|
+
if (shouldTransition) {
|
|
13126
|
+
const animation = element.animate(
|
|
13127
|
+
[
|
|
13128
|
+
{ left: `${previousLeft}px`, top: `${previousTop}px` },
|
|
13129
|
+
{ left: `${left}px`, top: `${top}px` },
|
|
13130
|
+
],
|
|
13131
|
+
{
|
|
13132
|
+
duration: parseTransitionDurationMs(
|
|
13133
|
+
element,
|
|
13134
|
+
"--popup-position-transition-duration",
|
|
13135
|
+
250,
|
|
13136
|
+
),
|
|
13137
|
+
easing: "ease",
|
|
13138
|
+
},
|
|
13139
|
+
);
|
|
13140
|
+
notifyPositionTransition(element, animation);
|
|
13141
|
+
}
|
|
13142
|
+
// The specified `left`/`top` are set to their final target right away,
|
|
13143
|
+
// regardless of `shouldTransition` — the animation above only plays the
|
|
13144
|
+
// visual move from the old position, it never becomes the actual
|
|
13145
|
+
// specified style (see notifyPositionTransition's own commitStyles for
|
|
13146
|
+
// why that matters once it ends).
|
|
13147
|
+
element.style.left = `${left}px`;
|
|
13148
|
+
element.style.top = `${top}px`;
|
|
13149
|
+
dispatchCustomEvent(element, "navi_position_change");
|
|
12609
13150
|
};
|
|
12610
13151
|
|
|
12611
13152
|
const [publishDebugger, subscribeDebugger] = createPubSub();
|
|
@@ -15858,4 +16399,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
|
|
|
15858
16399
|
};
|
|
15859
16400
|
};
|
|
15860
16401
|
|
|
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,
|
|
16402
|
+
export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, captureScrollState, chainEvent, closestOpenableAncestor, 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, findSelfOrAncestorFixedPosition, formatEventSideEffect, getAncestorOpenType, 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, getPositioningScrollOffset, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isAncestorOpen, isSameColor, isScrollable, measureLongestVisualLineWidth, measureScrollbar, measureWidestChildRow, mergeOneStyle, mergeTwoStyles, normalizeKeyboardKey, normalizeStyle, normalizeStyles, observeAncestorOpenState, onAncestorReopen, 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 };
|