SideEffect.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import * as React from 'react';
  2. import { RemoveScrollBar } from 'react-remove-scroll-bar';
  3. import { styleSingleton } from 'react-style-singleton';
  4. import { nonPassive } from './aggresiveCapture';
  5. import { handleScroll, locationCouldBeScrolled } from './handleScroll';
  6. export const getTouchXY = (event) => 'changedTouches' in event ? [event.changedTouches[0].clientX, event.changedTouches[0].clientY] : [0, 0];
  7. export const getDeltaXY = (event) => [event.deltaX, event.deltaY];
  8. const extractRef = (ref) => ref && 'current' in ref ? ref.current : ref;
  9. const deltaCompare = (x, y) => x[0] === y[0] && x[1] === y[1];
  10. const generateStyle = (id) => `
  11. .block-interactivity-${id} {pointer-events: none;}
  12. .allow-interactivity-${id} {pointer-events: all;}
  13. `;
  14. let idCounter = 0;
  15. let lockStack = [];
  16. export function RemoveScrollSideCar(props) {
  17. const shouldPreventQueue = React.useRef([]);
  18. const touchStartRef = React.useRef([0, 0]);
  19. const activeAxis = React.useRef();
  20. const [id] = React.useState(idCounter++);
  21. const [Style] = React.useState(() => styleSingleton());
  22. const lastProps = React.useRef(props);
  23. React.useEffect(() => {
  24. lastProps.current = props;
  25. }, [props]);
  26. React.useEffect(() => {
  27. if (props.inert) {
  28. document.body.classList.add(`block-interactivity-${id}`);
  29. const allow = [props.lockRef.current, ...(props.shards || []).map(extractRef)].filter(Boolean);
  30. allow.forEach((el) => el.classList.add(`allow-interactivity-${id}`));
  31. return () => {
  32. document.body.classList.remove(`block-interactivity-${id}`);
  33. allow.forEach((el) => el.classList.remove(`allow-interactivity-${id}`));
  34. };
  35. }
  36. return;
  37. }, [props.inert, props.lockRef.current, props.shards]);
  38. const shouldCancelEvent = React.useCallback((event, parent) => {
  39. if ('touches' in event && event.touches.length === 2) {
  40. return !lastProps.current.allowPinchZoom;
  41. }
  42. const touch = getTouchXY(event);
  43. const touchStart = touchStartRef.current;
  44. const deltaX = 'deltaX' in event ? event.deltaX : touchStart[0] - touch[0];
  45. const deltaY = 'deltaY' in event ? event.deltaY : touchStart[1] - touch[1];
  46. let currentAxis;
  47. const target = event.target;
  48. const moveDirection = Math.abs(deltaX) > Math.abs(deltaY) ? 'h' : 'v';
  49. // allow horizontal touch move on Range inputs. They will not cause any scroll
  50. if ('touches' in event && moveDirection === 'h' && target.type === 'range') {
  51. return false;
  52. }
  53. let canBeScrolledInMainDirection = locationCouldBeScrolled(moveDirection, target);
  54. if (!canBeScrolledInMainDirection) {
  55. return true;
  56. }
  57. if (canBeScrolledInMainDirection) {
  58. currentAxis = moveDirection;
  59. }
  60. else {
  61. currentAxis = moveDirection === 'v' ? 'h' : 'v';
  62. canBeScrolledInMainDirection = locationCouldBeScrolled(moveDirection, target);
  63. // other axis might be not scrollable
  64. }
  65. if (!canBeScrolledInMainDirection) {
  66. return false;
  67. }
  68. if (!activeAxis.current && 'changedTouches' in event && (deltaX || deltaY)) {
  69. activeAxis.current = currentAxis;
  70. }
  71. if (!currentAxis) {
  72. return true;
  73. }
  74. const cancelingAxis = activeAxis.current || currentAxis;
  75. return handleScroll(cancelingAxis, parent, event, cancelingAxis === 'h' ? deltaX : deltaY, true);
  76. }, []);
  77. const shouldPrevent = React.useCallback((_event) => {
  78. const event = _event;
  79. if (!lockStack.length || lockStack[lockStack.length - 1] !== Style) {
  80. // not the last active
  81. return;
  82. }
  83. const delta = 'deltaY' in event ? getDeltaXY(event) : getTouchXY(event);
  84. const sourceEvent = shouldPreventQueue.current.filter((e) => e.name === event.type && e.target === event.target && deltaCompare(e.delta, delta))[0];
  85. // self event, and should be canceled
  86. if (sourceEvent && sourceEvent.should) {
  87. if (event.cancelable) {
  88. event.preventDefault();
  89. }
  90. return;
  91. }
  92. // outside or shard event
  93. if (!sourceEvent) {
  94. const shardNodes = (lastProps.current.shards || [])
  95. .map(extractRef)
  96. .filter(Boolean)
  97. .filter((node) => node.contains(event.target));
  98. const shouldStop = shardNodes.length > 0 ? shouldCancelEvent(event, shardNodes[0]) : !lastProps.current.noIsolation;
  99. if (shouldStop) {
  100. if (event.cancelable) {
  101. event.preventDefault();
  102. }
  103. }
  104. }
  105. }, []);
  106. const shouldCancel = React.useCallback((name, delta, target, should) => {
  107. const event = { name, delta, target, should };
  108. shouldPreventQueue.current.push(event);
  109. setTimeout(() => {
  110. shouldPreventQueue.current = shouldPreventQueue.current.filter((e) => e !== event);
  111. }, 1);
  112. }, []);
  113. const scrollTouchStart = React.useCallback((event) => {
  114. touchStartRef.current = getTouchXY(event);
  115. activeAxis.current = undefined;
  116. }, []);
  117. const scrollWheel = React.useCallback((event) => {
  118. shouldCancel(event.type, getDeltaXY(event), event.target, shouldCancelEvent(event, props.lockRef.current));
  119. }, []);
  120. const scrollTouchMove = React.useCallback((event) => {
  121. shouldCancel(event.type, getTouchXY(event), event.target, shouldCancelEvent(event, props.lockRef.current));
  122. }, []);
  123. React.useEffect(() => {
  124. lockStack.push(Style);
  125. props.setCallbacks({
  126. onScrollCapture: scrollWheel,
  127. onWheelCapture: scrollWheel,
  128. onTouchMoveCapture: scrollTouchMove,
  129. });
  130. document.addEventListener('wheel', shouldPrevent, nonPassive);
  131. document.addEventListener('touchmove', shouldPrevent, nonPassive);
  132. document.addEventListener('touchstart', scrollTouchStart, nonPassive);
  133. return () => {
  134. lockStack = lockStack.filter((inst) => inst !== Style);
  135. document.removeEventListener('wheel', shouldPrevent, nonPassive);
  136. document.removeEventListener('touchmove', shouldPrevent, nonPassive);
  137. document.removeEventListener('touchstart', scrollTouchStart, nonPassive);
  138. };
  139. }, []);
  140. const { removeScrollBar, inert } = props;
  141. return (React.createElement(React.Fragment, null,
  142. inert ? React.createElement(Style, { styles: generateStyle(id) }) : null,
  143. removeScrollBar ? React.createElement(RemoveScrollBar, { gapMode: "margin" }) : null));
  144. }