Commit 2cc744d5 authored by Robert Knight's avatar Robert Knight

Add utilities for scrolling an element to a target offset

parent 3b9a71b3
/**
* Return a promise that resolves on the next animation frame.
*/
function nextAnimationFrame() {
return new Promise(resolve => {
requestAnimationFrame(resolve);
});
}
/**
* Linearly interpolate between two values.
*
* @param {number} a
* @param {number} b
* @param {number} fraction - Value in [0, 1]
*/
function interpolate(a, b, fraction) {
return a + fraction * (b - a);
}
/**
* Return the offset of `element` from the top of a positioned ancestor `parent`.
*
* @param {HTMLElement} element
* @param {HTMLElement} parent - Positioned ancestor of `element`
* @return {number}
*/
export function offsetRelativeTo(element, parent) {
let offset = 0;
while (element !== parent && parent.contains(element)) {
offset += element.offsetTop;
element = /** @type {HTMLElement} */ (element.offsetParent);
}
return offset;
}
/**
* Scroll `element` until its `scrollTop` offset reaches a target value.
*
* @param {Element} element - Container element to scroll
* @param {number} offset - Target value for the scroll offset
* @param {object} options
* @param {number} [options.maxDuration]
* @return {Promise<void>} - A promise that resolves once the scroll animation
* is complete
*/
export async function scrollElement(
element,
offset,
{ maxDuration = 500 } = {}
) {
const initialOffset = element.scrollTop;
const targetOffset = offset;
const scrollStart = Date.now();
// Choose a scroll duration proportional to the scroll distance, but capped
// to avoid it being too slow.
const pixelsPerMs = 3;
const scrollDuration = Math.min(
Math.abs(targetOffset - initialOffset) / pixelsPerMs,
maxDuration
);
let scrollFraction = 0.0;
while (scrollFraction < 1.0) {
await nextAnimationFrame();
scrollFraction = Math.min(1.0, (Date.now() - scrollStart) / scrollDuration);
element.scrollTop = interpolate(
initialOffset,
targetOffset,
scrollFraction
);
}
}
import { offsetRelativeTo, scrollElement } from '../scroll';
describe('annotator/util/scroll', () => {
let containers;
beforeEach(() => {
sinon.stub(window, 'requestAnimationFrame');
window.requestAnimationFrame.yields();
containers = [];
});
afterEach(() => {
containers.forEach(c => c.remove());
window.requestAnimationFrame.restore();
});
function createContainer() {
const el = document.createElement('div');
containers.push(el);
document.body.append(el);
return el;
}
describe('offsetRelativeTo', () => {
it('returns the offset of an element relative to the given ancestor', () => {
const parent = createContainer();
parent.style.position = 'relative';
const child = document.createElement('div');
child.style.position = 'absolute';
child.style.top = '100px';
parent.append(child);
const grandchild = document.createElement('div');
grandchild.style.position = 'absolute';
grandchild.style.top = '150px';
child.append(grandchild);
assert.equal(offsetRelativeTo(child, parent), 100);
assert.equal(offsetRelativeTo(grandchild, parent), 250);
});
it('returns 0 if the parent is not an ancestor of the element', () => {
const parent = document.createElement('div');
const child = document.createElement('div');
child.style.position = 'absolute';
child.style.top = '100px';
assert.equal(offsetRelativeTo(child, parent), 0);
});
});
describe('scrollElement', () => {
it("animates the element's `scrollTop` offset to the target position", async () => {
const container = createContainer();
container.style.overflow = 'scroll';
container.style.width = '200px';
container.style.height = '500px';
container.style.position = 'relative';
const child = document.createElement('div');
child.style.height = '3000px';
container.append(child);
await scrollElement(container, 2000, { maxDuration: 5 });
assert.equal(container.scrollTop, 2000);
container.remove();
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment