/ shared / components / src / utils / scrollByPolyfill.ts
scrollByPolyfill.ts
  1  // COPIED FROM
  2  // https://github.pie.apple.com/amp-ui/ember-ui-media-shelf/blob/580ff07a546771bce8b3d85494c6268860e97215/addon/-private/scroll-by-polyfill.js
  3  
  4  const SCROLL_TIME = 468;
  5  const Element =
  6      typeof window !== 'undefined' ? window.HTMLElement || window.Element : null;
  7  
  8  let originalScrollBy;
  9  
 10  /**
 11   * returns result of applying ease math function to a number
 12   * @method ease
 13   * @param {Number} k
 14   * @returns {Number}
 15   */
 16  function ease(k: number): number {
 17      return 0.5 * (1 - Math.cos(Math.PI * k));
 18  }
 19  
 20  // define timing method
 21  const now: () => number =
 22      typeof window !== 'undefined' && window?.performance?.now
 23          ? window.performance.now.bind(window.performance)
 24          : Date.now;
 25  
 26  /**
 27   * changes scroll position inside an element
 28   * @method scrollElement
 29   * @param {Number} x
 30   * @returns {undefined}
 31   */
 32  function scrollElement(x: number): void {
 33      this.scrollLeft = x;
 34  }
 35  
 36  /**
 37   * self invoked function that, given a context, steps through scrolling
 38   * @method step
 39   * @param {Object} context
 40   * @returns {undefined}
 41   */
 42  type Context = {
 43      startTime: number;
 44      startX: number;
 45      x: number;
 46      method: (x: number) => void;
 47      scrollable: HTMLElement;
 48  };
 49  function step(context: Context): void {
 50      const time = now();
 51      let elapsed = (time - context.startTime) / SCROLL_TIME;
 52  
 53      // avoid elapsed times higher than one
 54      elapsed = Math.min(1, elapsed);
 55  
 56      // apply easing to elapsed time
 57      const value = ease(elapsed);
 58  
 59      const currentX = context.startX + (context.x - context.startX) * value;
 60  
 61      context.method.call(context.scrollable, currentX);
 62  
 63      // scroll more if we have not reached our destination
 64      if (currentX !== context.x) {
 65          window.requestAnimationFrame(step.bind(window, context));
 66      }
 67  }
 68  
 69  /**
 70   * scrolls window or element with a smooth behavior
 71   * @method smoothScroll
 72   * @param {Object|Node} el
 73   * @param {Number} x
 74   * @returns {undefined}
 75   */
 76  function smoothScroll(el: HTMLElement, x: number): void {
 77      const startTime = now();
 78      // define scroll context
 79      const startX = el.scrollLeft;
 80      const method = scrollElement;
 81  
 82      // scroll looping over a frame
 83      step({
 84          scrollable: el,
 85          method,
 86          startTime,
 87          startX,
 88          x,
 89      });
 90  }
 91  
 92  let polyfillHasRun = false;
 93  /**
 94   * ripped partially from https://github.com/iamdustan/smoothscroll/blob/master/src/smoothscroll.js
 95   * Only polyfill horizontal scroll space to avoid unexpected behaviour in parent apps
 96   *
 97   * @method scrollByPolyfill
 98   */
 99  export default function scrollByPolyfill(): void {
100      // return if scroll behavior is supported
101      if ('scrollBehavior' in document.documentElement.style || polyfillHasRun) {
102          return;
103      }
104  
105      // if prefers-reduce-motion && need polyfill, navigate shelf immediately without easing
106      const motionMediaQuery = window.matchMedia(
107          '(prefers-reduced-motion: reduce)',
108      );
109      function addScrollByToProto() {
110          if (motionMediaQuery.matches) {
111              if (originalScrollBy) {
112                  Element.prototype.scrollBy = originalScrollBy;
113              }
114              return;
115          }
116  
117          function scrollByPoly(options: ScrollToOptions): void;
118          function scrollByPoly(x: number, _y: number): void;
119          // eslint-disable-next-line @typescript-eslint/no-unused-vars
120          function scrollByPoly(
121              paramOne: number | ScrollToOptions,
122              _paramTwo?: number,
123          ): void {
124              let xValue = 0;
125              if (typeof paramOne === 'number') {
126                  xValue = paramOne;
127              } else if (typeof paramOne === 'object') {
128                  xValue = paramOne.left || 0;
129              }
130  
131              const moveByX = this.scrollLeft + xValue;
132              smoothScroll(this, moveByX);
133          }
134  
135          originalScrollBy = Element.prototype.scrollBy;
136          Element.prototype.scrollBy = scrollByPoly;
137      }
138  
139      motionMediaQuery.addListener(addScrollByToProto);
140  
141      addScrollByToProto();
142      polyfillHasRun = true;
143  }