diff --git a/addons/web/static/src/core/utils/timing.js b/addons/web/static/src/core/utils/timing.js index 1ef844b1e01..cb6b8043dbb 100644 --- a/addons/web/static/src/core/utils/timing.js +++ b/addons/web/static/src/core/utils/timing.js @@ -107,6 +107,9 @@ export function setRecurringAnimationFrame(callback) { * Creates a version of the function where only the last call between two * animation frames is executed before the browser's next repaint. This * effectively throttles the function to the display's refresh rate. + * Note that the throttled function can be any callback. It is not + * specifically an event handler, no assumption is made about its + * signature. * NB: The first call is always called immediately (leading edge). * * @template {Function} T diff --git a/addons/web/static/tests/core/utils/timing_tests.js b/addons/web/static/tests/core/utils/timing_tests.js index 33edbe241b1..72b2865103f 100644 --- a/addons/web/static/tests/core/utils/timing_tests.js +++ b/addons/web/static/tests/core/utils/timing_tests.js @@ -512,4 +512,67 @@ QUnit.module("utils", () => { }); }); }); + + QUnit.test("throttleForAnimationScrollEvent", async (assert) => { + assert.expect(9); + const { advanceFrame, execRegisteredAnimationFrames } = mockAnimationFrame(); + let resolveThrottled; + let throttled = new Promise(resolve => resolveThrottled = resolve); + const throttledFn = throttleForAnimation((val, targetEl) => { + // In Chrome, the currentTarget of scroll events is lost after the + // event was handled, it is therefore null here. + // Because of this, if it is needed, it must be included in the + // callback signature. + const nodeName = val && val.currentTarget && val.currentTarget.nodeName; + const targetName = targetEl && targetEl.nodeName; + assert.step(`throttled function called with ${nodeName} in event, but ${targetName} in parameter`); + resolveThrottled(); + }); + + const el = document.createElement("div"); + el.style = "position: absolute; overflow: scroll; height: 100px; width: 100px;"; + const childEl = document.createElement("div"); + childEl.style = "height: 200px; width: 200px;"; + let resolveScrolled; + let scrolled = new Promise(resolve => resolveScrolled = resolve); + el.appendChild(childEl); + el.addEventListener("scroll", (ev) => { + assert.step("before scroll"); + throttledFn(ev, ev.currentTarget); + assert.step("after scroll"); + resolveScrolled(); + }); + document.body.appendChild(el); + el.scrollBy(1, 1); + el.scrollBy(2, 2); + await scrolled; + await throttled; + + assert.verifySteps([ + "before scroll", + "throttled function called with DIV in event, but DIV in parameter", + "after scroll", + ], "scroll happened and direct first call to throttled function happened too"); + + throttled = new Promise(resolve => resolveThrottled = resolve); + scrolled = new Promise(resolve => resolveScrolled = resolve); + el.scrollBy(3, 3); + await scrolled; + assert.verifySteps([ + "before scroll", + // Further call is delayed. + "after scroll", + ], "scroll happened but throttled function hasn't been called yet"); + setTimeout(() => { + advanceFrame(); + execRegisteredAnimationFrames(); + }); + await throttled; + assert.verifySteps( + ["throttled function called with null in event, but DIV in parameter"], + "currentTarget was not available in throttled function's event" + ); + el.remove(); + }); + });