Files
apps.apple.com/shared/components/src/utils/scrollByPolyfill.ts
2025-11-04 05:03:50 +08:00

144 lines
3.9 KiB
TypeScript

// 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;
}