123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- 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) {
- 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;
- }
- 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 && 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 };
- 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, { gapMode: "margin" }) : null));
- }
|