1 | const getScrollingRoot = () => document.scrollingElement || document.documentElement;
|
2 |
|
3 | /**
|
4 | * Recursively finds the scroll parent of a node. The scroll parrent of a node
|
5 | * is the closest node that is scrollable. A node is scrollable if:
|
6 | * - it is allowed to scroll via CSS ('overflow-y' not visible or hidden);
|
7 | * - and its children/content are "bigger" than the node's box height.
|
8 | *
|
9 | * The root of the document always scrolls by default.
|
10 | *
|
11 | * @param {HTMLElement} node Any DOM element.
|
12 | * @return {HTMLElement} The scroll parent element.
|
13 | */
|
14 | export function getScrollParent(node) {
|
15 | const parent = node.parentElement;
|
16 |
|
17 | if (parent == null) return getScrollingRoot();
|
18 |
|
19 | const { overflowY } = window.getComputedStyle(parent);
|
20 | const canScroll = overflowY !== 'visible' && overflowY !== 'hidden';
|
21 |
|
22 | if (canScroll && parent.scrollHeight > parent.clientHeight) {
|
23 | return parent;
|
24 | }
|
25 |
|
26 | return getScrollParent(parent);
|
27 | }
|
28 |
|
29 | /**
|
30 | * Recursively traverses the tree upwards from the given node, capturing all
|
31 | * ancestor nodes that scroll along with their current 'overflow-y' CSS
|
32 | * property.
|
33 | *
|
34 | * @param {HTMLElement} node Any DOM element.
|
35 | * @param {Map<HTMLElement,string>} [acc] Accumulator map.
|
36 | * @return {Map<HTMLElement,string>} Map of ancestors with their 'overflow-y' value.
|
37 | */
|
38 | export function getScrollAncestorsOverflowY(node, acc = new Map()) {
|
39 | const scrollingRoot = getScrollingRoot();
|
40 | const scrollParent = getScrollParent(node);
|
41 | acc.set(scrollParent, scrollParent.style.overflowY);
|
42 |
|
43 | if (scrollParent === scrollingRoot) return acc;
|
44 | return getScrollAncestorsOverflowY(scrollParent, acc);
|
45 | }
|
46 |
|
47 | /**
|
48 | * Disabling the scroll on a node involves finding all the scrollable ancestors
|
49 | * and set their 'overflow-y' CSS property to 'hidden'. When all ancestors have
|
50 | * 'overflow-y: hidden' (up to the document element) there is no scroll
|
51 | * container, thus all the scroll outside of the node is disabled. In order to
|
52 | * enable scroll again, we store the previous value of the 'overflow-y' for
|
53 | * every ancestor in a closure and reset it back.
|
54 | *
|
55 | * @param {HTMLElement} node Any DOM element.
|
56 | */
|
57 | export default function disableScroll(node) {
|
58 | const scrollAncestorsOverflowY = getScrollAncestorsOverflowY(node);
|
59 | const toggle = (on) => scrollAncestorsOverflowY.forEach((overflowY, ancestor) => {
|
60 | ancestor.style.setProperty('overflow-y', on ? 'hidden' : overflowY);
|
61 | });
|
62 |
|
63 | toggle(true);
|
64 | return () => toggle(false);
|
65 | }
|