diff --git a/client/src/utils/transition.test.js b/client/src/utils/transition.test.js new file mode 100644 index 0000000000..9cffb98040 --- /dev/null +++ b/client/src/utils/transition.test.js @@ -0,0 +1,83 @@ +import { transition } from './transition'; + +jest.useFakeTimers(); + +describe('transition', () => { + beforeAll(() => { + document.body.innerHTML = `
`; + }); + + it('should resolve the immediately if maxDelay is 0 or falsy', async () => { + expect( + await transition(document.getElementById('main'), { maxDelay: 0 }), + ).toBe(null); + + expect( + await transition(document.getElementById('main'), { maxDelay: false }), + ).toBe(null); + + expect( + await transition(document.getElementById('main'), { maxDelay: -1 }), + ).toBe(null); + }); + + it('should resolve after the maxDelay (350ms) if no events are fired', async () => { + const resolve = jest.fn(); + + transition(document.getElementById('main')).then(resolve); + + await jest.advanceTimersByTimeAsync(200); + + expect(resolve).not.toHaveBeenCalled(); + + await jest.advanceTimersByTimeAsync(149); + + expect(resolve).not.toHaveBeenCalled(); + + await jest.advanceTimersByTimeAsync(1); + + expect(resolve).toHaveBeenCalledWith(null); + }); + + it('should resolve if transitionend or animationend is fired', async () => { + const resolve = jest.fn(); + + transition(document.getElementById('main')).then(resolve); + + await jest.advanceTimersByTimeAsync(200); + + expect(resolve).not.toHaveBeenCalled(); + + const event = new Event('transitionend', { + bubbles: true, + cancelable: false, + }); + + document.getElementById('main').dispatchEvent(event); + + await jest.advanceTimersByTimeAsync(0); + + expect(resolve).toHaveBeenCalledWith(event); + }); + + it('should resolve if animationend is fired', async () => { + const resolve = jest.fn(); + + transition(document.getElementById('main')).then(resolve); + + await jest.advanceTimersByTimeAsync(200); + + expect(resolve).not.toHaveBeenCalled(); + + const event = new Event('animationend', { + bubbles: true, + cancelable: false, + }); + + document.getElementById('main').dispatchEvent(event); + + await jest.advanceTimersByTimeAsync(0); + + expect(resolve).toHaveBeenCalledWith(event); + }); +}); diff --git a/client/src/utils/transition.ts b/client/src/utils/transition.ts new file mode 100644 index 0000000000..ac4fc25b1d --- /dev/null +++ b/client/src/utils/transition.ts @@ -0,0 +1,27 @@ +/** + * Returns a promise that will resolve after either the animation, transition + * or the max delay of time is reached. + * + * If `maxDelay` is provided as zero or a falsy value, the promise resolve immediately. + */ +export const transition = (element: HTMLElement, { maxDelay = 350 } = {}) => + new Promise((resolve) => { + if (!maxDelay || maxDelay <= 0) { + resolve(null); + return; + } + + let timer: number | undefined; + + const finish = (event: AnimationEvent | TransitionEvent | null) => { + if (event && event.target !== element) return; + window.clearTimeout(timer); + element.removeEventListener('transitionend', finish); + element.removeEventListener('animationend', finish); + resolve(event || null); + }; + + element.addEventListener('animationend', finish); + element.addEventListener('transitionend', finish); + timer = window.setTimeout(finish, maxDelay); + });