@helpscout/hsds-react
Version:
React component library for Help Scout's Design System
170 lines (139 loc) • 5.74 kB
JavaScript
exports.__esModule = true;
exports.default = useFancyAnimationScroller;
exports.exponentialDecay = exponentialDecay;
exports.bindRequestAnimationFrame = bindRequestAnimationFrame;
var _react = require("react");
var _useMeasureNode = require("./useMeasureNode");
// very difficult to test with JSDom, some basic interaction is tested in ScrollableContainer
/* istanbul ignore file */
function useFancyAnimationScroller(_ref) {
var container = _ref.container,
_ref$decayRates = _ref.decayRates,
decayRates = _ref$decayRates === void 0 ? [0.01, 0.05] : _ref$decayRates,
nodeToAnimateFinalHeight = _ref.nodeToAnimateFinalHeight,
nodeToAnimateSelector = _ref.nodeToAnimateSelector,
nodeThatScrollsSelector = _ref.nodeThatScrollsSelector,
_ref$topReachedClassN = _ref.topReachedClassNames,
topReachedClassNames = _ref$topReachedClassN === void 0 ? 'at-the-top' : _ref$topReachedClassN;
var initialHeight = (0, _react.useRef)(0);
var lastScrollPosition = (0, _react.useRef)(0);
var decayUp = decayRates[0],
decayDown = decayRates[1];
var containerNode = getElement(container);
if (containerNode) {
var nodeToAnimate = containerNode.querySelector(nodeToAnimateSelector);
var resizeObserver = (0, _useMeasureNode.setupObserver)({
cb: function cb(_ref2) {
var height = _ref2.height;
if (lastScrollPosition.current === 0) {
var _nodeToAnimate$classL;
initialHeight.current = height;
(_nodeToAnimate$classL = nodeToAnimate.classList).add.apply(_nodeToAnimate$classL, [].concat(topReachedClassNames));
} else if (initialHeight.current === 0) {
initialHeight.current = height;
}
},
dimensions: {
height: true
}
});
resizeObserver.observe(nodeToAnimate);
}
(0, _react.useEffect)(function () {
/**
* Browsers behave differently when "remembering" the scroll position
* of elements, for example for some reason Chrome doesn't remember
* on this component while firefox does.
* Here we make it consistent by just making them all "forget".
*/
window.addEventListener('unload', restoreScroll);
return function () {
window.removeEventListener('unload', restoreScroll);
}; // eslint-disable-next-line react-hooks/exhaustive-deps
}, [containerNode]);
function restoreScroll() {
var scrollableNode = containerNode.querySelector(nodeThatScrollsSelector);
scrollableNode.scrollTop = 0;
}
function handleScroll() {
var scrollableNode = containerNode.querySelector(nodeThatScrollsSelector);
var scrollableFullHeight = scrollableNode.scrollHeight;
var scrollTop = scrollableNode.scrollTop;
var direction = lastScrollPosition.current < scrollTop ? 'down' : 'up';
lastScrollPosition.current = scrollTop;
var nodeToAnimateInitialHeight = initialHeight.current;
var nodeToAnimate = containerNode.querySelector(nodeToAnimateSelector);
var nodeToAnimateCurrentHeight = nodeToAnimate.getBoundingClientRect().height;
if (direction === 'down') {
if (nodeToAnimateCurrentHeight > nodeToAnimateFinalHeight) {
var rate = exponentialDecay(decayDown, scrollableFullHeight)(scrollTop);
var percentage = rate * 100 / scrollableFullHeight;
var progress = percentage * (nodeToAnimateInitialHeight - nodeToAnimateFinalHeight) / 100;
var newHeight = nodeToAnimateInitialHeight - progress;
nodeToAnimate.style.height = newHeight + "px";
if (newHeight >= nodeToAnimateFinalHeight * 0.75) {
var _nodeToAnimate$classL2;
(_nodeToAnimate$classL2 = nodeToAnimate.classList).remove.apply(_nodeToAnimate$classL2, [].concat(topReachedClassNames));
}
}
} else if (direction === 'up') {
if (nodeToAnimateCurrentHeight <= nodeToAnimateInitialHeight) {
var _rate = exponentialDecay(decayUp, scrollableFullHeight)(scrollTop);
var _percentage = 100 - _rate * 100 / scrollableFullHeight;
var _progress = nodeToAnimateInitialHeight * (_percentage / 100);
var _newHeight = _progress < nodeToAnimateFinalHeight ? nodeToAnimateFinalHeight : _progress;
if (scrollTop !== 0) {
nodeToAnimate.style.height = _newHeight + "px";
} else {
nodeToAnimate.style.height = null;
}
if (_newHeight === nodeToAnimateInitialHeight) {
var _nodeToAnimate$classL3;
(_nodeToAnimate$classL3 = nodeToAnimate.classList).add.apply(_nodeToAnimate$classL3, [].concat(topReachedClassNames));
}
}
}
}
return [bindRequestAnimationFrame(handleScroll, true)];
}
function getElement(someRef) {
if (someRef instanceof HTMLElement) return someRef;
return someRef && someRef.current;
}
/**
* Calculates a number from a scale of exponential decay at a given rate.
* @param {Number} rate The rate of decay, the larger the number the quickest the decay
* @param {Number} upper The limit value
* @returns Number
*/
function exponentialDecay(rate, upper) {
return function (t) {
return upper * (1 - Math.exp(-(rate * t)));
};
}
/**
* From https://stackoverflow.com/a/44779316
*
* @param {Function} fn Callback function
* @param {Boolean|undefined} [throttle] Optionally throttle callback
* @return {Function} Bound function
*/
function bindRequestAnimationFrame(fn, throttle) {
var isRunning;
var that;
var args;
var run = function run() {
isRunning = false;
fn.apply(that, args);
};
return function () {
that = this;
args = arguments;
if (isRunning && throttle) {
return;
}
isRunning = true;
requestAnimationFrame(run);
};
}
;