Skip to content

Instantly share code, notes, and snippets.

@alexreardon
Last active October 24, 2023 22:15
Show Gist options
  • Save alexreardon/03b0b68ce858832caaffdb02000591f9 to your computer and use it in GitHub Desktop.
Save alexreardon/03b0b68ce858832caaffdb02000591f9 to your computer and use it in GitHub Desktop.
Element.prototype.scrollBy ponyfill (for testing)
// This file polyfills `Element.prototype.scrollBy`
// scrollBy(x-coord, y-coord)
// scrollBy(options)
(() => {
if (typeof Element === 'undefined') {
return;
}
if (typeof Element.prototype.scrollBy !== 'undefined') {
return;
}
// Fire a scroll event in a future task
// (in browsers this is close to `process.nextTick`
// which is what Chrome has now for `setTimeout(fn, 0)`)
// Trying to match browser behaviour as closely as possible
// https://codesandbox.io/s/how-scroll-events-flow-through-nested-scroll-containers-qgsd2v?file=/src/index.ts
const scheduleScroll = (() => {
let isScrollEventQueued = false;
const sharedQueue = [];
return function schedule(target) {
sharedQueue.push(target);
if (isScrollEventQueued) {
return;
}
setTimeout(() => {
// Shallow cloning the array before iterating to avoid an infinite loop
// if the queue is added to while events are being dispatched
const items = Array.from(sharedQueue);
sharedQueue.length = 0;
items.forEach(item => {
item.dispatchEvent(new Event('scroll'));
});
isScrollEventQueued = false;
}, 0);
isScrollEventQueued = true;
};
})();
function getOptions(...args) {
// scrollBy(options)
if (args.length === 1 && typeof args[0] === 'object') {
return args[0];
}
// scrollBy(x-coord, y-coord)
// (it's okay if `top` or `left` are `undefined`)
return {
// x-coord
top: args[0],
// y-coord
left: args[1],
};
}
function scrollBy(...args) {
const options = getOptions(...args);
// no scroll event is triggered if no scroll occurs
if (options.top === 0 && options.left === 0) {
return;
}
// Expecting `this` to be `Element`
this.scrollTop = (() => {
const original = this.scrollTop;
// no change to top - can exit early
if (options.top === 0) {
return original;
}
// Note: not rounding options.top as chrome supports scrolling by partial pixels
const change = options.top;
const updated = Math.max(original + change, 0);
// clientHeight is set to 0 by default by jsdom.
// We can only correctly set a maximum scrollTop if clientHeight is set.
if (this.clientHeight === 0) {
return updated;
}
const maxScrollTop = this.scrollHeight - this.clientHeight;
return Math.min(updated, maxScrollTop);
})();
this.scrollLeft = (() => {
const original = this.scrollLeft;
// no change to left - can exit early
if (options.left === 0) {
return original;
}
// Note: not rounding options.top as chrome supports scrolling by partial pixels
const change = options.left;
const updated = Math.max(original + change, 0);
// clientWidth is set to 0 by default by jsdom.
// We can only correctly set a maximum scrollTop if clientWidth is set.
if (this.clientWidth === 0) {
return updated;
}
const maxScrollLeft = this.scrollWidth - this.clientWidth;
return Math.min(updated, maxScrollLeft);
})();
scheduleScroll(this);
}
Element.prototype.scrollBy = scrollBy;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment