diff --git a/addon-test-support/@ember/test-helpers/dom/scroll-to.ts b/addon-test-support/@ember/test-helpers/dom/scroll-to.ts new file mode 100644 index 000000000..d44e29f61 --- /dev/null +++ b/addon-test-support/@ember/test-helpers/dom/scroll-to.ts @@ -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} resolves when settled + + @example + + Scroll DOM element to specific coordinates + + + 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 { + 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(); + }); +} diff --git a/addon-test-support/@ember/test-helpers/index.ts b/addon-test-support/@ember/test-helpers/index.ts index b1c85f4e7..ad7bb410a 100644 --- a/addon-test-support/@ember/test-helpers/index.ts +++ b/addon-test-support/@ember/test-helpers/index.ts @@ -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'; diff --git a/tests/integration/dom/scroll-to-test.js b/tests/integration/dom/scroll-to-test.js new file mode 100644 index 000000000..30356512f --- /dev/null +++ b/tests/integration/dom/scroll-to-test.js @@ -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` +
+
    +
  • A
  • +
  • B
  • +
  • C
  • +
  • D
  • +
  • E
  • +
+
+ `); + + 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` +
+
A
+
B
+
C
+
D
+
E
+
+ `); + + 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`
`); + + 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`
`); + + 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/); + }); +});