summaryrefslogtreecommitdiff
path: root/shared/components/src/utils/scrollByPolyfill.ts
blob: 1a73a4f8bdc9a7225b333f88322e48fed0c68ffc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// COPIED FROM
// https://github.pie.apple.com/amp-ui/ember-ui-media-shelf/blob/580ff07a546771bce8b3d85494c6268860e97215/addon/-private/scroll-by-polyfill.js

const SCROLL_TIME = 468;
const Element =
    typeof window !== 'undefined' ? window.HTMLElement || window.Element : null;

let originalScrollBy;

/**
 * returns result of applying ease math function to a number
 * @method ease
 * @param {Number} k
 * @returns {Number}
 */
function ease(k: number): number {
    return 0.5 * (1 - Math.cos(Math.PI * k));
}

// define timing method
const now: () => number =
    typeof window !== 'undefined' && window?.performance?.now
        ? window.performance.now.bind(window.performance)
        : Date.now;

/**
 * changes scroll position inside an element
 * @method scrollElement
 * @param {Number} x
 * @returns {undefined}
 */
function scrollElement(x: number): void {
    this.scrollLeft = x;
}

/**
 * self invoked function that, given a context, steps through scrolling
 * @method step
 * @param {Object} context
 * @returns {undefined}
 */
type Context = {
    startTime: number;
    startX: number;
    x: number;
    method: (x: number) => void;
    scrollable: HTMLElement;
};
function step(context: Context): void {
    const time = now();
    let elapsed = (time - context.startTime) / SCROLL_TIME;

    // avoid elapsed times higher than one
    elapsed = Math.min(1, elapsed);

    // apply easing to elapsed time
    const value = ease(elapsed);

    const currentX = context.startX + (context.x - context.startX) * value;

    context.method.call(context.scrollable, currentX);

    // scroll more if we have not reached our destination
    if (currentX !== context.x) {
        window.requestAnimationFrame(step.bind(window, context));
    }
}

/**
 * scrolls window or element with a smooth behavior
 * @method smoothScroll
 * @param {Object|Node} el
 * @param {Number} x
 * @returns {undefined}
 */
function smoothScroll(el: HTMLElement, x: number): void {
    const startTime = now();
    // define scroll context
    const startX = el.scrollLeft;
    const method = scrollElement;

    // scroll looping over a frame
    step({
        scrollable: el,
        method,
        startTime,
        startX,
        x,
    });
}

let polyfillHasRun = false;
/**
 * ripped partially from https://github.com/iamdustan/smoothscroll/blob/master/src/smoothscroll.js
 * Only polyfill horizontal scroll space to avoid unexpected behaviour in parent apps
 *
 * @method scrollByPolyfill
 */
export default function scrollByPolyfill(): void {
    // return if scroll behavior is supported
    if ('scrollBehavior' in document.documentElement.style || polyfillHasRun) {
        return;
    }

    // if prefers-reduce-motion && need polyfill, navigate shelf immediately without easing
    const motionMediaQuery = window.matchMedia(
        '(prefers-reduced-motion: reduce)',
    );
    function addScrollByToProto() {
        if (motionMediaQuery.matches) {
            if (originalScrollBy) {
                Element.prototype.scrollBy = originalScrollBy;
            }
            return;
        }

        function scrollByPoly(options: ScrollToOptions): void;
        function scrollByPoly(x: number, _y: number): void;
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        function scrollByPoly(
            paramOne: number | ScrollToOptions,
            _paramTwo?: number,
        ): void {
            let xValue = 0;
            if (typeof paramOne === 'number') {
                xValue = paramOne;
            } else if (typeof paramOne === 'object') {
                xValue = paramOne.left || 0;
            }

            const moveByX = this.scrollLeft + xValue;
            smoothScroll(this, moveByX);
        }

        originalScrollBy = Element.prototype.scrollBy;
        Element.prototype.scrollBy = scrollByPoly;
    }

    motionMediaQuery.addListener(addScrollByToProto);

    addScrollByToProto();
    polyfillHasRun = true;
}