SideEffect.js 7.45 KB
import * as React from 'react';
import { RemoveScrollBar } from 'react-remove-scroll-bar';
import { styleSingleton } from 'react-style-singleton';
import { nonPassive } from './aggresiveCapture';
import { handleScroll, locationCouldBeScrolled } from './handleScroll';
export const getTouchXY = (event) => 'changedTouches' in event ? [event.changedTouches[0].clientX, event.changedTouches[0].clientY] : [0, 0];
export const getDeltaXY = (event) => [event.deltaX, event.deltaY];
const extractRef = (ref) => ref && 'current' in ref ? ref.current : ref;
const deltaCompare = (x, y) => x[0] === y[0] && x[1] === y[1];
const generateStyle = (id) => `
  .block-interactivity-${id} {pointer-events: none;}
  .allow-interactivity-${id} {pointer-events: all;}
`;
let idCounter = 0;
let lockStack = [];
export function RemoveScrollSideCar(props) {
    const shouldPreventQueue = React.useRef([]);
    const touchStartRef = React.useRef([0, 0]);
    const activeAxis = React.useRef();
    const [id] = React.useState(idCounter++);
    const [Style] = React.useState(styleSingleton);
    const lastProps = React.useRef(props);
    React.useEffect(() => {
        lastProps.current = props;
    }, [props]);
    React.useEffect(() => {
        if (props.inert) {
            document.body.classList.add(`block-interactivity-${id}`);
            const allow = [props.lockRef.current, ...(props.shards || []).map(extractRef)].filter(Boolean);
            allow.forEach((el) => el.classList.add(`allow-interactivity-${id}`));
            return () => {
                document.body.classList.remove(`block-interactivity-${id}`);
                allow.forEach((el) => el.classList.remove(`allow-interactivity-${id}`));
            };
        }
        return;
    }, [props.inert, props.lockRef.current, props.shards]);
    const shouldCancelEvent = React.useCallback((event, parent) => {
        if (('touches' in event && event.touches.length === 2) || (event.type === 'wheel' && event.ctrlKey)) {
            return !lastProps.current.allowPinchZoom;
        }
        const touch = getTouchXY(event);
        const touchStart = touchStartRef.current;
        const deltaX = 'deltaX' in event ? event.deltaX : touchStart[0] - touch[0];
        const deltaY = 'deltaY' in event ? event.deltaY : touchStart[1] - touch[1];
        let currentAxis;
        const target = event.target;
        const moveDirection = Math.abs(deltaX) > Math.abs(deltaY) ? 'h' : 'v';
        // allow horizontal touch move on Range inputs. They will not cause any scroll
        if ('touches' in event && moveDirection === 'h' && target.type === 'range') {
            return false;
        }
        // allow drag selection (iOS); check if selection's anchorNode is the same as target or contains target
        const selection = window.getSelection();
        const anchorNode = selection && selection.anchorNode;
        const isTouchingSelection = anchorNode ? anchorNode === target || anchorNode.contains(target) : false;
        if (isTouchingSelection) {
            return false;
        }
        let canBeScrolledInMainDirection = locationCouldBeScrolled(moveDirection, target);
        if (!canBeScrolledInMainDirection) {
            return true;
        }
        if (canBeScrolledInMainDirection) {
            currentAxis = moveDirection;
        }
        else {
            currentAxis = moveDirection === 'v' ? 'h' : 'v';
            canBeScrolledInMainDirection = locationCouldBeScrolled(moveDirection, target);
            // other axis might be not scrollable
        }
        if (!canBeScrolledInMainDirection) {
            return false;
        }
        if (!activeAxis.current && 'changedTouches' in event && (deltaX || deltaY)) {
            activeAxis.current = currentAxis;
        }
        if (!currentAxis) {
            return true;
        }
        const cancelingAxis = activeAxis.current || currentAxis;
        return handleScroll(cancelingAxis, parent, event, cancelingAxis === 'h' ? deltaX : deltaY, true);
    }, []);
    const shouldPrevent = React.useCallback((_event) => {
        const event = _event;
        if (!lockStack.length || lockStack[lockStack.length - 1] !== Style) {
            // not the last active
            return;
        }
        const delta = 'deltaY' in event ? getDeltaXY(event) : getTouchXY(event);
        const sourceEvent = shouldPreventQueue.current.filter((e) => e.name === event.type && (e.target === event.target || event.target === e.shadowParent) && deltaCompare(e.delta, delta))[0];
        // self event, and should be canceled
        if (sourceEvent && sourceEvent.should) {
            if (event.cancelable) {
                event.preventDefault();
            }
            return;
        }
        // outside or shard event
        if (!sourceEvent) {
            const shardNodes = (lastProps.current.shards || [])
                .map(extractRef)
                .filter(Boolean)
                .filter((node) => node.contains(event.target));
            const shouldStop = shardNodes.length > 0 ? shouldCancelEvent(event, shardNodes[0]) : !lastProps.current.noIsolation;
            if (shouldStop) {
                if (event.cancelable) {
                    event.preventDefault();
                }
            }
        }
    }, []);
    const shouldCancel = React.useCallback((name, delta, target, should) => {
        const event = { name, delta, target, should, shadowParent: getOutermostShadowParent(target) };
        shouldPreventQueue.current.push(event);
        setTimeout(() => {
            shouldPreventQueue.current = shouldPreventQueue.current.filter((e) => e !== event);
        }, 1);
    }, []);
    const scrollTouchStart = React.useCallback((event) => {
        touchStartRef.current = getTouchXY(event);
        activeAxis.current = undefined;
    }, []);
    const scrollWheel = React.useCallback((event) => {
        shouldCancel(event.type, getDeltaXY(event), event.target, shouldCancelEvent(event, props.lockRef.current));
    }, []);
    const scrollTouchMove = React.useCallback((event) => {
        shouldCancel(event.type, getTouchXY(event), event.target, shouldCancelEvent(event, props.lockRef.current));
    }, []);
    React.useEffect(() => {
        lockStack.push(Style);
        props.setCallbacks({
            onScrollCapture: scrollWheel,
            onWheelCapture: scrollWheel,
            onTouchMoveCapture: scrollTouchMove,
        });
        document.addEventListener('wheel', shouldPrevent, nonPassive);
        document.addEventListener('touchmove', shouldPrevent, nonPassive);
        document.addEventListener('touchstart', scrollTouchStart, nonPassive);
        return () => {
            lockStack = lockStack.filter((inst) => inst !== Style);
            document.removeEventListener('wheel', shouldPrevent, nonPassive);
            document.removeEventListener('touchmove', shouldPrevent, nonPassive);
            document.removeEventListener('touchstart', scrollTouchStart, nonPassive);
        };
    }, []);
    const { removeScrollBar, inert } = props;
    return (React.createElement(React.Fragment, null,
        inert ? React.createElement(Style, { styles: generateStyle(id) }) : null,
        removeScrollBar ? React.createElement(RemoveScrollBar, { noRelative: props.noRelative, gapMode: props.gapMode }) : null));
}
function getOutermostShadowParent(node) {
    let shadowParent = null;
    while (node !== null) {
        if (node instanceof ShadowRoot) {
            shadowParent = node.host;
            node = node.host;
        }
        node = node.parentNode;
    }
    return shadowParent;
}