Last active
October 24, 2023 22:15
-
-
Save alexreardon/03b0b68ce858832caaffdb02000591f9 to your computer and use it in GitHub Desktop.
Element.prototype.scrollBy ponyfill (for testing)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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