Skip to content

Commit

Permalink
Add scroll-to helper
Browse files Browse the repository at this point in the history
  • Loading branch information
nlfurniss committed Apr 20, 2020
1 parent 637fdbc commit a99326d
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 0 deletions.
55 changes: 55 additions & 0 deletions addon-test-support/@ember/test-helpers/dom/scroll-to.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import getElement from './-get-element';
import fireEvent from './fire-event';
import settled from '../settled';
import { nextTickPromise } from '../-utils';
import { isElement } from './-target';

/**
Scrolls DOM element or selector to the given coordinates.
@public
@param {string|HTMLElement} target the element or selector to trigger scroll on
@param {Number} x x-coordinate
@param {Number} y y-coordinate
@return {Promise<void>} resolves when settled
@example
<caption>
Scroll DOM element to specific coordinates
</caption>
scrollTo('#my-long-div', 0, 0); // scroll to top
scrollTo('#my-long-div', 0, 100); // scroll down
*/
export default function scrollTo(
target: string | HTMLElement,
x: number,
y: number
): Promise<void> {
return nextTickPromise().then(() => {
if (!target) {
throw new Error('Must pass an element or selector to `scrollTo`.');
}

if (x === undefined || y === undefined) {
throw new Error('Must pass both x and y coordinates to `scrollTo`.');
}

let element = getElement(target);
if (!element) {
throw new Error(`Element not found when calling \`scrollTo('${target}')\`.`);
}

if (!isElement(element)) {
throw new Error(
`"target" must be an element, but was a ${element.nodeType} when calling \`scrollTo('${target}')\`.`
);
}

element.scrollTop = y;
element.scrollLeft = x;

fireEvent(element, 'scroll');

return settled();
});
}
1 change: 1 addition & 0 deletions addon-test-support/@ember/test-helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ export { default as getRootElement } from './dom/get-root-element';
export { default as find } from './dom/find';
export { default as findAll } from './dom/find-all';
export { default as typeIn } from './dom/type-in';
export { default as scrollTo } from './dom/scroll-to';
114 changes: 114 additions & 0 deletions tests/integration/dom/scroll-to-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { module, test } from 'qunit';
import hbs from 'htmlbars-inline-precompile';
import {
render,
scrollTo,
setupContext,
setupRenderingContext,
teardownContext,
} from '@ember/test-helpers';
import hasEmberVersion from '@ember/test-helpers/has-ember-version';

module('DOM Helper: scroll-to', function (hooks) {
if (!hasEmberVersion(2, 4)) {
return;
}

hooks.beforeEach(async function () {
await setupContext(this);
await setupRenderingContext(this);
});

hooks.afterEach(async function () {
await teardownContext(this);
});

test('Scroll in vertical direction', async function (assert) {
assert.expect(1);

let currentScrollPosition = 0;
let scrollAmount = 50;
this.callback = e => {
currentScrollPosition = e.target.scrollTop;
};

await render(hbs`
<div
style="height: 200px; overflow-y: auto;"
class="container"
onscroll={{action callback}}
>
<ul>
<li class="item" style="height: 100px;">A</li>
<li class="item" style="height: 100px;">B</li>
<li class="item" style="height: 100px;">C</li>
<li class="item" style="height: 100px;">D</li>
<li class="item" style="height: 100px;">E</li>
</ul>
</div>
`);

await scrollTo('.container', 0, scrollAmount);

assert.equal(
currentScrollPosition,
scrollAmount,
'After use of the `scrollTop` a paint cycle is triggered and the callback is called'
);
});

test('Scroll in horizontal direction', async function (assert) {
let currentScrollPosition = 0;
let scrollAmount = 50;
this.callback = e => {
currentScrollPosition = e.target.scrollLeft;
};

await render(hbs`
<div
style="width: 200px; overflow-x: auto; white-space: nowrap;"
class="container"
onscroll={{action callback}}
>
<div class="item" style="width: 100px; height: 100px; display: inline-block">A</div>
<div class="item" style="width: 100px; height: 100px; display: inline-block">B</div>
<div class="item" style="width: 100px; height: 100px; display: inline-block">C</div>
<div class="item" style="width: 100px; height: 100px; display: inline-block">D</div>
<div class="item" style="width: 100px; height: 100px; display: inline-block">E</div>
</div>
`);

await scrollTo('.container', scrollAmount, 0);

assert.equal(
currentScrollPosition,
scrollAmount,
'After use of the `scrollLeft` a paint cycle is triggered and the callback is called'
);
});

test('It throws an error if a target is not supplied', async function (assert) {
assert.rejects(
scrollTo('', 0, 0),
new Error('Must pass an element or selector to `scrollTo`.')
);
});

test('It throws an error if all coordinates are not supplied', async function (assert) {
await render(hbs`<div class="container"></div>`);

assert.rejects(scrollTo('.container', 0), /Must pass both x and y coordinates/);
assert.rejects(scrollTo('.container', undefined, 0), /Must pass both x and y coordinates/);
});

test('It throws an error if the target is not found', async function (assert) {
await render(hbs`<div class="container"></div>`);

assert.rejects(scrollTo('.container2', 0, 0), /Element not found when calling/);
});

test('It throws an error if the target is not an element', async function (assert) {
assert.rejects(scrollTo(document, 0, 0), /"target" must be an element/);
assert.rejects(scrollTo(window, 0, 0), /"target" must be an element/);
});
});

0 comments on commit a99326d

Please sign in to comment.