handleScroll.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. const alwaysContainsScroll = (node) =>
  2. // textarea will always _contain_ scroll inside self. It only can be hidden
  3. node.tagName === 'TEXTAREA';
  4. const elementCanBeScrolled = (node, overflow) => {
  5. const styles = window.getComputedStyle(node);
  6. return (
  7. // not-not-scrollable
  8. styles[overflow] !== 'hidden' &&
  9. // contains scroll inside self
  10. !(styles.overflowY === styles.overflowX && !alwaysContainsScroll(node) && styles[overflow] === 'visible'));
  11. };
  12. const elementCouldBeVScrolled = (node) => elementCanBeScrolled(node, 'overflowY');
  13. const elementCouldBeHScrolled = (node) => elementCanBeScrolled(node, 'overflowX');
  14. export const locationCouldBeScrolled = (axis, node) => {
  15. let current = node;
  16. do {
  17. // Skip over shadow root
  18. if (typeof ShadowRoot !== 'undefined' && current instanceof ShadowRoot) {
  19. current = current.host;
  20. }
  21. const isScrollable = elementCouldBeScrolled(axis, current);
  22. if (isScrollable) {
  23. const [, s, d] = getScrollVariables(axis, current);
  24. if (s > d) {
  25. return true;
  26. }
  27. }
  28. current = current.parentNode;
  29. } while (current && current !== document.body);
  30. return false;
  31. };
  32. const getVScrollVariables = ({ scrollTop, scrollHeight, clientHeight }) => [
  33. scrollTop,
  34. scrollHeight,
  35. clientHeight,
  36. ];
  37. const getHScrollVariables = ({ scrollLeft, scrollWidth, clientWidth }) => [
  38. scrollLeft,
  39. scrollWidth,
  40. clientWidth,
  41. ];
  42. const elementCouldBeScrolled = (axis, node) => axis === 'v' ? elementCouldBeVScrolled(node) : elementCouldBeHScrolled(node);
  43. const getScrollVariables = (axis, node) => axis === 'v' ? getVScrollVariables(node) : getHScrollVariables(node);
  44. const getDirectionFactor = (axis, direction) =>
  45. /**
  46. * If the element's direction is rtl (right-to-left), then scrollLeft is 0 when the scrollbar is at its rightmost position,
  47. * and then increasingly negative as you scroll towards the end of the content.
  48. * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
  49. */
  50. axis === 'h' && direction === 'rtl' ? -1 : 1;
  51. export const handleScroll = (axis, endTarget, event, sourceDelta, noOverscroll) => {
  52. const directionFactor = getDirectionFactor(axis, window.getComputedStyle(endTarget).direction);
  53. const delta = directionFactor * sourceDelta;
  54. // find scrollable target
  55. let target = event.target;
  56. const targetInLock = endTarget.contains(target);
  57. let shouldCancelScroll = false;
  58. const isDeltaPositive = delta > 0;
  59. let availableScroll = 0;
  60. let availableScrollTop = 0;
  61. do {
  62. const [position, scroll, capacity] = getScrollVariables(axis, target);
  63. const elementScroll = scroll - capacity - directionFactor * position;
  64. if (position || elementScroll) {
  65. if (elementCouldBeScrolled(axis, target)) {
  66. availableScroll += elementScroll;
  67. availableScrollTop += position;
  68. }
  69. }
  70. target = target.parentNode;
  71. } while (
  72. // portaled content
  73. (!targetInLock && target !== document.body) ||
  74. // self content
  75. (targetInLock && (endTarget.contains(target) || endTarget === target)));
  76. if (isDeltaPositive && ((noOverscroll && availableScroll === 0) || (!noOverscroll && delta > availableScroll))) {
  77. shouldCancelScroll = true;
  78. }
  79. else if (!isDeltaPositive &&
  80. ((noOverscroll && availableScrollTop === 0) || (!noOverscroll && -delta > availableScrollTop))) {
  81. shouldCancelScroll = true;
  82. }
  83. return shouldCancelScroll;
  84. };