Skip to content

feat(packages/utils): added scroll-to #52

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ module.exports = {
// globalTeardown: undefined,

// A set of global variables that need to be available in all test environments
// globals: {},
globals: {
window: true
},

// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
Expand Down Expand Up @@ -138,7 +140,7 @@ module.exports = {
// snapshotSerializers: [],

// The test environment that will be used for testing
testEnvironment: 'node',
testEnvironment: 'jsdom',

// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
Expand Down
3 changes: 3 additions & 0 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"peerDependencies": {
"@alfalab/data": "^0.2.0"
},
"dependencies": {
"bezier-easing": "2.1.0"
},
"publishConfig": {
"access": "public"
}
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './crop-account-number';
export * from './pluralize';
export * from './get-countries';
export * from './format-phone';
export * from './scroll-to';
35 changes: 35 additions & 0 deletions packages/utils/src/scroll-to/easings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import bezierEasing from 'bezier-easing';

const easings = {
easeInSine: bezierEasing(0.47, 0, 0.745, 0.715),
easeOutSine: bezierEasing(0.39, 0.575, 0.565, 1),
easeInOutSine: bezierEasing(0.445, 0.05, 0.55, 0.95),
easeInQuad: bezierEasing(0.55, 0.085, 0.68, 0.53),
easeOutQuad: bezierEasing(0.25, 0.46, 0.45, 0.94),
easeInOutQuad: bezierEasing(0.455, 0.03, 0.515, 0.955),
easeInCubic: bezierEasing(0.55, 0.055, 0.675, 0.19),
easeOutCubic: bezierEasing(0.215, 0.61, 0.355, 1),
easeInOutCubic: bezierEasing(0.645, 0.045, 0.355, 1),
easeInQuart: bezierEasing(0.895, 0.03, 0.685, 0.22),
easeOutQuart: bezierEasing(0.165, 0.84, 0.44, 1),
easeInOutQuart: bezierEasing(0.77, 0, 0.175, 1),
easeInQuint: bezierEasing(0.755, 0.05, 0.855, 0.06),
easeOutQuint: bezierEasing(0.23, 1, 0.32, 1),
easeInOutQuint: bezierEasing(0.86, 0, 0.07, 1),
easeInExpo: bezierEasing(0.95, 0.05, 0.795, 0.035),
easeOutExpo: bezierEasing(0.19, 1, 0.22, 1),
easeInOutExpo: bezierEasing(1, 0, 0, 1),
easeInCirc: bezierEasing(0.6, 0.04, 0.98, 0.335),
easeOutCirc: bezierEasing(0.075, 0.82, 0.165, 1),
easeInOutCirc: bezierEasing(0.785, 0.135, 0.15, 0.86),
easeInBack: bezierEasing(0.6, -0.28, 0.735, 0.045),
easeOutBack: bezierEasing(0.175, 0.885, 0.32, 1.275),
easeInOutBack: bezierEasing(0.68, -0.55, 0.265, 1.55),
};

export type EasingType = keyof typeof easings;
export default easings;
1 change: 1 addition & 0 deletions packages/utils/src/scroll-to/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './util';
114 changes: 114 additions & 0 deletions packages/utils/src/scroll-to/util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import scrollTo from './util';

function getContainer() {
return document.getElementById('container') as HTMLElement;
}

function getScrollContainer() {
return document.getElementById('scroll-container') as HTMLElement;
}

function getScrollContainerChild(n: number) {
return getScrollContainer().children[n] as HTMLElement;
}

describe('scroll-to', () => {
const originalWindowScrollTo = window.scrollTo;

beforeEach(() => {
const domContainerNode = document.createElement('div');

domContainerNode.setAttribute('id', 'container');
domContainerNode.setAttribute('style', 'min-height: 9999px; margin: 0; padding: 0;');
domContainerNode.innerHTML = `
<div id='scroll-container' style='overflow: auto; width: 500px; height: 1000px;'>
<div style='width: 100%; height: 300px;'></div>
<div style='width: 100%; height: 300px;'></div>
<div style='width: 100%; height: 300px;'></div>
<div style='width: 100%; height: 300px;'></div>
<div style='width: 100%; height: 300px;'></div>
</div>
`;
document.body.appendChild(domContainerNode);

window.scrollTo = jest.fn();
});

afterEach(() => {
const domContainerNode = getContainer();

document.body.removeChild(domContainerNode);
window.scrollTo = originalWindowScrollTo;
});

it('should scroll to Y in window', async () => {
await scrollTo({
targetY: 100,
duration: 200,
});
expect(window.scrollTo).toHaveBeenCalledWith(0, 100);
});

it('should scroll to Y in container', async () => {
const container = getScrollContainer();

await scrollTo({
targetY: 100,
container,
});

expect(container.scrollTop).toBe(100);
});

it('should catch error with incorrect easing', () => {
const fn = function () {
scrollTo({
targetY: 100,
duration: 200,
easing: 'incorrectEase',
} as any);
};

expect(fn).toThrow('Incorrect easing in options');
});

it('should catch error with incorrect duration', () => {
const fn = function () {
scrollTo({
targetY: 100,
duration: -200,
});
};

expect(fn).toThrow('Incorrect duration in options');
});

it('should scroll down to element in container', async () => {
const element = getScrollContainerChild(3);
const container = getScrollContainer();
const correction = element.offsetHeight;

if (element.offsetTop + correction > container.scrollTop + container.offsetHeight) {
await scrollTo({ container, targetY: element.offsetTop });
expect(container.scrollTop).toBe(500);
}
});

it('should scroll up to element in container', async () => {
const element = getScrollContainerChild(0);
const container = getScrollContainer();
const correction = element.offsetHeight;

container.scrollTop = 500;

if (element.offsetTop < container.scrollTop) {
await scrollTo({ container, targetY: (element.offsetTop - container.offsetHeight) + correction });

expect(container.scrollTop).toBe(0);
}
});
});
86 changes: 86 additions & 0 deletions packages/utils/src/scroll-to/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import easings, { EasingType } from './easings';

export const SCROLL_TO_CORRECTION = 16;
export const SCROLL_TO_DEFAULT_DURATION = 0;
export const SCROLL_TO_NORMAL_DURATION = 250;
export const SCROLL_TO_EASING = 'easeInOutSine';

type ScrollToOptions = {
/**
* Цель по оси Y
*/
targetY: number;
/**
* Элемент в котором скроллим
*/
container?: HTMLElement;
/**
* Продолжительность анимации в миллесекундах
*/
duration?: number;
/**
* Название функции плавности для анимации
*/
easing?: EasingType;
}
/**
* Скроллит по элементу или странице.
* В настоящее время доступно перемещение только по оси Y.
* TODO: Make a move on the x axis
*/
export default function scrollTo({
targetY,
container,
duration = 0,
easing = SCROLL_TO_EASING,
}: ScrollToOptions): Promise<void> {
const scrollY = container ? container.scrollTop : window.pageYOffset;
const startTime = window.performance.now();

if (duration < 0) {
throw new Error('Incorrect duration in options');
}

if (!easings[easing]) {
throw new Error('Incorrect easing in options');
}

const easingFunc = easings[easing];

return new Promise((resolve) => {
function scrollToTarget(y: number): void {
if (container) {
// eslint-disable-next-line no-param-reassign
container.scrollTop = y;
} else {
window.scrollTo(0, y);
}
}

function loop(timestamp: number): void {
const currentTime = Math.abs(timestamp - startTime);
const t = currentTime / duration;
const val = easingFunc(t);
const currentTargetY = scrollY + ((targetY - scrollY) * val);

if (t < 1) {
window.requestAnimationFrame(loop);
scrollToTarget(currentTargetY);
} else {
scrollToTarget(targetY);
resolve();
}
}

if (duration === 0) {
scrollToTarget(targetY);
resolve();
} else {
loop(window.performance.now());
}
});
}
Loading