@jsenv/dom 0.17.25 → 0.17.26
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 +191 -1
- package/package.json +1 -1
package/dist/jsenv_dom.js
CHANGED
|
@@ -7674,11 +7674,187 @@ const getPaddingSizes = (element) => {
|
|
|
7674
7674
|
};
|
|
7675
7675
|
};
|
|
7676
7676
|
|
|
7677
|
+
// A gesture on an element the browser paints in the top layer belongs to that
|
|
7678
|
+
// element, wherever it sits in the DOM.
|
|
7679
|
+
const TOP_LAYER_SELECTOR$1 = [
|
|
7680
|
+
":popover-open",
|
|
7681
|
+
"dialog:modal",
|
|
7682
|
+
":fullscreen",
|
|
7683
|
+
].join(",");
|
|
7684
|
+
|
|
7685
|
+
// The keys that scroll the document when nothing focusable holds them.
|
|
7686
|
+
const SCROLL_KEY_SET = new Set([
|
|
7687
|
+
"ArrowUp",
|
|
7688
|
+
"ArrowDown",
|
|
7689
|
+
"ArrowLeft",
|
|
7690
|
+
"ArrowRight",
|
|
7691
|
+
"PageUp",
|
|
7692
|
+
"PageDown",
|
|
7693
|
+
"Home",
|
|
7694
|
+
"End",
|
|
7695
|
+
" ",
|
|
7696
|
+
]);
|
|
7697
|
+
|
|
7698
|
+
/**
|
|
7699
|
+
* Stops the background from scrolling by cancelling the scroll gestures that
|
|
7700
|
+
* would reach it, leaving every style on the page untouched.
|
|
7701
|
+
*
|
|
7702
|
+
* **Why not overflow: hidden?**
|
|
7703
|
+
* A scroll container that cannot scroll has no sticky offsets: browsers only
|
|
7704
|
+
* apply `position: sticky` constraints to a scroller that can actually scroll.
|
|
7705
|
+
* Locking the document with `overflow: hidden` therefore drops every stuck
|
|
7706
|
+
* element (a sticky header, a sticky table column) back to its flow position —
|
|
7707
|
+
* off screen, since the page stays visually scrolled where it was. Cancelling
|
|
7708
|
+
* gestures keeps the scroller scrollable, so sticky, the scrollbar and the
|
|
7709
|
+
* layout width all stay exactly as they are (no gutter to compensate).
|
|
7710
|
+
*
|
|
7711
|
+
* **What it does not stop**
|
|
7712
|
+
* - dragging the native scrollbar: it sits outside the viewport, no backdrop
|
|
7713
|
+
* covers it and no event of ours is involved;
|
|
7714
|
+
* - programmatic scroll (`scrollTo`, `scrollIntoView`, a focus moving into the
|
|
7715
|
+
* background).
|
|
7716
|
+
* Both are deliberate acts on a background that is covered by a backdrop, hence
|
|
7717
|
+
* the requirement: without something covering the background, lock the overflow
|
|
7718
|
+
* instead (see `scroll_trap.js`).
|
|
7719
|
+
*
|
|
7720
|
+
* @param {HTMLElement} element - The overlay being shown. Gestures landing on
|
|
7721
|
+
* it (or on any other top layer element) are left alone.
|
|
7722
|
+
* @param {Object} [options]
|
|
7723
|
+
* @param {HTMLElement} [options.boundaryElement] - Only block gestures landing
|
|
7724
|
+
* inside this element. For an overlay confined to a local container: the
|
|
7725
|
+
* container must stop scrolling, the rest of the page keeps its gestures.
|
|
7726
|
+
* @param {HTMLElement} [options.backdropElement] - The element covering the
|
|
7727
|
+
* background, when it is one of ours (a modal dialog covers it with its own
|
|
7728
|
+
* `::backdrop` and there is nothing to pass). It is in the top layer like the
|
|
7729
|
+
* overlay itself, yet a gesture on it aims at the background behind it.
|
|
7730
|
+
* @returns {() => void} Cleanup function removing the listeners.
|
|
7731
|
+
*/
|
|
7732
|
+
|
|
7733
|
+
const trapScrollGestureInside = (
|
|
7734
|
+
element,
|
|
7735
|
+
{ boundaryElement, backdropElement } = {},
|
|
7736
|
+
) => {
|
|
7737
|
+
const { ownerDocument } = element;
|
|
7738
|
+
const lockedRegion = boundaryElement || ownerDocument.documentElement;
|
|
7739
|
+
|
|
7740
|
+
const isBackgroundGesture = (target, clientX, clientY) => {
|
|
7741
|
+
if (!lockedRegion.contains(target)) {
|
|
7742
|
+
return false;
|
|
7743
|
+
}
|
|
7744
|
+
if (element.contains(target)) {
|
|
7745
|
+
// A modal <dialog> is the event target for the whole backdrop area too
|
|
7746
|
+
// (::backdrop is not hit-testable), so being the target does not mean the
|
|
7747
|
+
// pointer is on it — the rect decides, same as the backdrop-click
|
|
7748
|
+
// detection in dialog.jsx.
|
|
7749
|
+
if (target !== element) {
|
|
7750
|
+
return false;
|
|
7751
|
+
}
|
|
7752
|
+
const { left, right, top, bottom } = element.getBoundingClientRect();
|
|
7753
|
+
return (
|
|
7754
|
+
clientX < left || clientX > right || clientY < top || clientY > bottom
|
|
7755
|
+
);
|
|
7756
|
+
}
|
|
7757
|
+
if (backdropElement && backdropElement.contains(target)) {
|
|
7758
|
+
return true;
|
|
7759
|
+
}
|
|
7760
|
+
let ancestorOrSelf = target;
|
|
7761
|
+
while (ancestorOrSelf && ancestorOrSelf.nodeType === 1) {
|
|
7762
|
+
if (ancestorOrSelf.matches(TOP_LAYER_SELECTOR$1)) {
|
|
7763
|
+
return false;
|
|
7764
|
+
}
|
|
7765
|
+
ancestorOrSelf = ancestorOrSelf.parentNode;
|
|
7766
|
+
}
|
|
7767
|
+
return true;
|
|
7768
|
+
};
|
|
7769
|
+
|
|
7770
|
+
const onWheel = (wheelEvent) => {
|
|
7771
|
+
if (wheelEvent.ctrlKey) {
|
|
7772
|
+
// Browser zoom, not a scroll — cancelling it would take zoom away from
|
|
7773
|
+
// the page while an overlay is open.
|
|
7774
|
+
return;
|
|
7775
|
+
}
|
|
7776
|
+
if (
|
|
7777
|
+
isBackgroundGesture(
|
|
7778
|
+
wheelEvent.target,
|
|
7779
|
+
wheelEvent.clientX,
|
|
7780
|
+
wheelEvent.clientY,
|
|
7781
|
+
)
|
|
7782
|
+
) {
|
|
7783
|
+
wheelEvent.preventDefault();
|
|
7784
|
+
}
|
|
7785
|
+
};
|
|
7786
|
+
const onTouchMove = (touchMoveEvent) => {
|
|
7787
|
+
const { touches } = touchMoveEvent;
|
|
7788
|
+
if (touches.length !== 1) {
|
|
7789
|
+
return; // pinch
|
|
7790
|
+
}
|
|
7791
|
+
const [touch] = touches;
|
|
7792
|
+
if (
|
|
7793
|
+
isBackgroundGesture(touchMoveEvent.target, touch.clientX, touch.clientY)
|
|
7794
|
+
) {
|
|
7795
|
+
touchMoveEvent.preventDefault();
|
|
7796
|
+
}
|
|
7797
|
+
};
|
|
7798
|
+
const onKeyDown = (keydownEvent) => {
|
|
7799
|
+
if (!SCROLL_KEY_SET.has(keydownEvent.key)) {
|
|
7800
|
+
return;
|
|
7801
|
+
}
|
|
7802
|
+
const { target } = keydownEvent;
|
|
7803
|
+
// Anything else focused (a field, a scrollable box, a button) owns the key
|
|
7804
|
+
// press; only a document with nothing focused scrolls on it.
|
|
7805
|
+
if (
|
|
7806
|
+
target !== ownerDocument.body &&
|
|
7807
|
+
target !== ownerDocument.documentElement
|
|
7808
|
+
) {
|
|
7809
|
+
return;
|
|
7810
|
+
}
|
|
7811
|
+
if (isBackgroundGesture(target)) {
|
|
7812
|
+
keydownEvent.preventDefault();
|
|
7813
|
+
}
|
|
7814
|
+
};
|
|
7815
|
+
|
|
7816
|
+
// Capture phase: the gesture must be cancelled before whatever it landed on
|
|
7817
|
+
// gets a chance to act on it. passive: false is what makes preventDefault
|
|
7818
|
+
// effective at all on wheel/touchmove.
|
|
7819
|
+
const listenerOptions = { capture: true, passive: false };
|
|
7820
|
+
ownerDocument.addEventListener("wheel", onWheel, listenerOptions);
|
|
7821
|
+
ownerDocument.addEventListener("touchmove", onTouchMove, listenerOptions);
|
|
7822
|
+
ownerDocument.addEventListener("keydown", onKeyDown, listenerOptions);
|
|
7823
|
+
return () => {
|
|
7824
|
+
ownerDocument.removeEventListener("wheel", onWheel, listenerOptions);
|
|
7825
|
+
ownerDocument.removeEventListener(
|
|
7826
|
+
"touchmove",
|
|
7827
|
+
onTouchMove,
|
|
7828
|
+
listenerOptions,
|
|
7829
|
+
);
|
|
7830
|
+
ownerDocument.removeEventListener("keydown", onKeyDown, listenerOptions);
|
|
7831
|
+
};
|
|
7832
|
+
};
|
|
7833
|
+
|
|
7834
|
+
// "gesture": cancel the scroll gestures aimed at the background, keeping it
|
|
7835
|
+
// scrollable (see scroll_gesture_trap.js) — only possible when a backdrop
|
|
7836
|
+
// covers it.
|
|
7837
|
+
// "overflow": hide the overflow of every scroll container behind the overlay.
|
|
7838
|
+
// Set this to "overflow" to put every scroll lock on that single strategy.
|
|
7839
|
+
const SCROLL_LOCK_STRATEGY = "gesture";
|
|
7840
|
+
|
|
7677
7841
|
/**
|
|
7678
7842
|
* Prevents scrolling on all scrollable containers that are ancestors of (or
|
|
7679
7843
|
* siblings preceding) `element`. Used when an overlay (popover, dialog) is
|
|
7680
7844
|
* open and background scroll should be disabled.
|
|
7681
7845
|
*
|
|
7846
|
+
* **Two strategies, and why the gesture one comes first**
|
|
7847
|
+
* Hiding the overflow makes the scroller unable to scroll, and a scroller that
|
|
7848
|
+
* cannot scroll has no sticky offsets: every `position: sticky` element behind
|
|
7849
|
+
* the overlay drops back to its flow position — off screen, since the page
|
|
7850
|
+
* stays visually scrolled where it was. So whenever a backdrop covers the
|
|
7851
|
+
* background (`backdrop`), the lock is done by cancelling scroll gestures
|
|
7852
|
+
* (`scroll_gesture_trap.js`): nothing in the page moves, sticky included, and
|
|
7853
|
+
* there is no scrollbar to compensate for. The overflow lock below is what
|
|
7854
|
+
* remains when nothing covers the background — a gesture trap would then let
|
|
7855
|
+
* the user scroll by dragging the scrollbar of a container they can still see
|
|
7856
|
+
* and reach. `SCROLL_LOCK_STRATEGY` forces that overflow lock everywhere.
|
|
7857
|
+
*
|
|
7682
7858
|
* **Why padding instead of scrollbar-gutter?**
|
|
7683
7859
|
* `scrollbar-gutter: stable` would be the modern, CSS-native way to reserve
|
|
7684
7860
|
* the scrollbar lane before hiding overflow so the layout doesn't shift.
|
|
@@ -7700,9 +7876,23 @@ const getPaddingSizes = (element) => {
|
|
|
7700
7876
|
* inside this element (itself included). For an overlay confined to a local
|
|
7701
7877
|
* container rather than the viewport: the container's own scroll must stop,
|
|
7702
7878
|
* the rest of the page keeps scrolling as usual.
|
|
7879
|
+
* @param {true|HTMLElement} [options.backdrop] - What covers the background
|
|
7880
|
+
* while the overlay is open: the backdrop element, or `true` when the browser
|
|
7881
|
+
* paints it itself (a modal dialog's `::backdrop`). Enables the gesture
|
|
7882
|
+
* strategy described above.
|
|
7703
7883
|
* @returns {() => void} Cleanup function that restores all modified styles.
|
|
7704
7884
|
*/
|
|
7705
|
-
const trapScrollInside = (
|
|
7885
|
+
const trapScrollInside = (
|
|
7886
|
+
element,
|
|
7887
|
+
{ boundaryElement, backdrop } = {},
|
|
7888
|
+
) => {
|
|
7889
|
+
if (backdrop && SCROLL_LOCK_STRATEGY === "gesture") {
|
|
7890
|
+
return trapScrollGestureInside(element, {
|
|
7891
|
+
boundaryElement,
|
|
7892
|
+
backdropElement: backdrop === true ? null : backdrop,
|
|
7893
|
+
});
|
|
7894
|
+
}
|
|
7895
|
+
|
|
7706
7896
|
const cleanupCallbackSet = new Set();
|
|
7707
7897
|
|
|
7708
7898
|
// Collect every element to lock first (preceding scrollable siblings + all
|